email_util.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import 'dart:io';
  2. import 'package:archive/archive_io.dart';
  3. import 'package:email_validator/email_validator.dart';
  4. import "package:file_saver/file_saver.dart";
  5. import 'package:flutter/cupertino.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:flutter_email_sender/flutter_email_sender.dart';
  9. import "package:intl/intl.dart";
  10. import 'package:logging/logging.dart';
  11. import 'package:open_mail_app/open_mail_app.dart';
  12. import 'package:package_info_plus/package_info_plus.dart';
  13. import 'package:path_provider/path_provider.dart';
  14. import 'package:photos/core/configuration.dart';
  15. import 'package:photos/core/error-reporting/super_logging.dart';
  16. import "package:photos/generated/l10n.dart";
  17. import 'package:photos/ui/components/buttons/button_widget.dart';
  18. import 'package:photos/ui/components/dialog_widget.dart';
  19. import 'package:photos/ui/components/models/button_type.dart';
  20. import 'package:photos/ui/tools/debug/log_file_viewer.dart';
  21. import 'package:photos/utils/dialog_util.dart';
  22. import 'package:photos/utils/toast_util.dart';
  23. import 'package:share_plus/share_plus.dart';
  24. import 'package:url_launcher/url_launcher.dart';
  25. final Logger _logger = Logger('email_util');
  26. bool isValidEmail(String? email) {
  27. if (email == null) {
  28. return false;
  29. }
  30. return EmailValidator.validate(email);
  31. }
  32. Future<void> sendLogs(
  33. BuildContext context,
  34. String title,
  35. String toEmail, {
  36. Function? postShare,
  37. String? subject,
  38. String? body,
  39. }) async {
  40. showDialogWidget(
  41. context: context,
  42. title: S.of(context).reportABug,
  43. icon: Icons.bug_report_outlined,
  44. body: S.of(context).logsDialogBody,
  45. buttons: [
  46. ButtonWidget(
  47. isInAlert: true,
  48. buttonType: ButtonType.neutral,
  49. labelText: S.of(context).reportABug,
  50. buttonAction: ButtonAction.first,
  51. shouldSurfaceExecutionStates: false,
  52. onTap: () async {
  53. await _sendLogs(context, toEmail, subject, body);
  54. if (postShare != null) {
  55. postShare();
  56. }
  57. },
  58. ),
  59. //isInAlert is false here as we don't want to the dialog to dismiss
  60. //on pressing this button
  61. ButtonWidget(
  62. buttonType: ButtonType.secondary,
  63. labelText: S.of(context).viewLogs,
  64. buttonAction: ButtonAction.second,
  65. onTap: () async {
  66. showDialog(
  67. context: context,
  68. builder: (BuildContext context) {
  69. return LogFileViewer(SuperLogging.logFile!);
  70. },
  71. barrierColor: Colors.black87,
  72. barrierDismissible: false,
  73. );
  74. },
  75. ),
  76. ButtonWidget(
  77. buttonType: ButtonType.secondary,
  78. labelText: S.of(context).exportLogs,
  79. buttonAction: ButtonAction.third,
  80. onTap: () async {
  81. final zipFilePath = await getZippedLogsFile(context);
  82. await exportLogs(context, zipFilePath);
  83. },
  84. ),
  85. ButtonWidget(
  86. isInAlert: true,
  87. buttonType: ButtonType.secondary,
  88. labelText: S.of(context).cancel,
  89. buttonAction: ButtonAction.cancel,
  90. ),
  91. ],
  92. );
  93. }
  94. Future<void> _sendLogs(
  95. BuildContext context,
  96. String toEmail,
  97. String? subject,
  98. String? body,
  99. ) async {
  100. final String zipFilePath = await getZippedLogsFile(context);
  101. final Email email = Email(
  102. recipients: [toEmail],
  103. subject: subject ?? '',
  104. body: body ?? '',
  105. attachmentPaths: [zipFilePath],
  106. isHTML: false,
  107. );
  108. try {
  109. await FlutterEmailSender.send(email);
  110. } catch (e, s) {
  111. _logger.severe('email sender failed', e, s);
  112. Navigator.of(context, rootNavigator: true).pop();
  113. await shareLogs(context, toEmail, zipFilePath);
  114. }
  115. }
  116. Future<String> getZippedLogsFile(BuildContext context) async {
  117. final dialog = createProgressDialog(context, S.of(context).preparingLogs);
  118. await dialog.show();
  119. final logsPath = (await getApplicationSupportDirectory()).path;
  120. final logsDirectory = Directory(logsPath + "/logs");
  121. final tempPath = (await getTemporaryDirectory()).path;
  122. final zipFilePath =
  123. tempPath + "/logs-${Configuration.instance.getUserID() ?? 0}.zip";
  124. final encoder = ZipFileEncoder();
  125. encoder.create(zipFilePath);
  126. encoder.addDirectory(logsDirectory);
  127. encoder.close();
  128. await dialog.hide();
  129. return zipFilePath;
  130. }
  131. Future<void> shareLogs(
  132. BuildContext context,
  133. String toEmail,
  134. String zipFilePath,
  135. ) async {
  136. final result = await showDialogWidget(
  137. context: context,
  138. title: S.of(context).emailYourLogs,
  139. body: S.of(context).pleaseSendTheLogsTo(toEmail),
  140. buttons: [
  141. ButtonWidget(
  142. buttonType: ButtonType.neutral,
  143. labelText: S.of(context).copyEmailAddress,
  144. isInAlert: true,
  145. buttonAction: ButtonAction.first,
  146. onTap: () async {
  147. await Clipboard.setData(ClipboardData(text: toEmail));
  148. },
  149. shouldShowSuccessConfirmation: true,
  150. ),
  151. ButtonWidget(
  152. buttonType: ButtonType.neutral,
  153. labelText: S.of(context).exportLogs,
  154. isInAlert: true,
  155. buttonAction: ButtonAction.second,
  156. ),
  157. ButtonWidget(
  158. buttonType: ButtonType.secondary,
  159. labelText: S.of(context).cancel,
  160. isInAlert: true,
  161. buttonAction: ButtonAction.cancel,
  162. ),
  163. ],
  164. );
  165. if (result?.action != null && result!.action == ButtonAction.second) {
  166. await exportLogs(context, zipFilePath);
  167. }
  168. }
  169. Future<void> exportLogs(BuildContext context, String zipFilePath) async {
  170. final Size size = MediaQuery.of(context).size;
  171. if (Platform.isAndroid) {
  172. final DateTime now = DateTime.now().toUtc();
  173. final String shortMonthName = DateFormat('MMM').format(now); // Short month
  174. final String logFileName =
  175. 'ente-logs-${now.year}-$shortMonthName-${now.day}-${now.hour}-${now.minute}';
  176. await FileSaver.instance.saveAs(
  177. name: logFileName,
  178. filePath: zipFilePath,
  179. mimeType: MimeType.zip,
  180. ext: 'zip',
  181. );
  182. } else {
  183. await Share.shareXFiles(
  184. [XFile(zipFilePath, mimeType: 'application/zip')],
  185. sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2),
  186. );
  187. }
  188. }
  189. Future<void> sendEmail(
  190. BuildContext context, {
  191. required String to,
  192. String? subject,
  193. String? body,
  194. }) async {
  195. try {
  196. final String clientDebugInfo = await _clientInfo();
  197. final EmailContent emailContent = EmailContent(
  198. to: [
  199. to,
  200. ],
  201. subject: subject ?? '[Support]',
  202. body: (body ?? '') + clientDebugInfo,
  203. );
  204. if (Platform.isAndroid) {
  205. // Special handling due to issue in proton mail android client
  206. // https://github.com/ente-io/photos-app/pull/253
  207. final Uri params = Uri(
  208. scheme: 'mailto',
  209. path: to,
  210. query: 'subject=${emailContent.subject}&body=${emailContent.body}',
  211. );
  212. if (await canLaunchUrl(params)) {
  213. await launchUrl(params);
  214. } else {
  215. // this will trigger _showNoMailAppsDialog
  216. throw Exception('Could not launch ${params.toString()}');
  217. }
  218. } else {
  219. final OpenMailAppResult result =
  220. await OpenMailApp.composeNewEmailInMailApp(
  221. nativePickerTitle: 'Select emailContent app',
  222. emailContent: emailContent,
  223. );
  224. if (!result.didOpen && !result.canOpen) {
  225. _showNoMailAppsDialog(context, to);
  226. } else if (!result.didOpen && result.canOpen) {
  227. await showCupertinoModalPopup(
  228. context: context,
  229. builder: (_) => CupertinoActionSheet(
  230. title: Text("Select mail app \n $to"),
  231. actions: [
  232. for (var app in result.options)
  233. CupertinoActionSheetAction(
  234. child: Text(app.name),
  235. onPressed: () {
  236. final content = emailContent;
  237. OpenMailApp.composeNewEmailInSpecificMailApp(
  238. mailApp: app,
  239. emailContent: content,
  240. );
  241. Navigator.of(context, rootNavigator: true).pop();
  242. },
  243. ),
  244. ],
  245. cancelButton: CupertinoActionSheetAction(
  246. child: Text(S.of(context).cancel),
  247. onPressed: () {
  248. Navigator.of(context, rootNavigator: true).pop();
  249. },
  250. ),
  251. ),
  252. );
  253. }
  254. }
  255. } catch (e) {
  256. _logger.severe("Failed to send emailContent to $to", e);
  257. _showNoMailAppsDialog(context, to);
  258. }
  259. }
  260. Future<String> _clientInfo() async {
  261. final packageInfo = await PackageInfo.fromPlatform();
  262. final String debugInfo =
  263. '\n\n\n\n ------------------- \nFollowing information can '
  264. 'help us in debugging if you are facing any issue '
  265. '\nRegistered email: ${Configuration.instance.getEmail()}'
  266. '\nClient: ${packageInfo.packageName}'
  267. '\nVersion : ${packageInfo.version}';
  268. return debugInfo;
  269. }
  270. void _showNoMailAppsDialog(BuildContext context, String toEmail) {
  271. showChoiceDialog(
  272. context,
  273. icon: Icons.email_outlined,
  274. title: S.of(context).pleaseEmailUsAt(toEmail),
  275. firstButtonLabel: S.of(context).copyEmailAddress,
  276. secondButtonLabel: S.of(context).dismiss,
  277. firstButtonOnTap: () async {
  278. await Clipboard.setData(ClipboardData(text: toEmail));
  279. showShortToast(context, 'Copied');
  280. },
  281. );
  282. }