email_util.dart 8.9 KB

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