email_util.dart 9.2 KB

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