email_util.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import 'dart:io';
  2. import 'package:archive/archive_io.dart';
  3. import 'package:email_validator/email_validator.dart';
  4. import 'package:flutter/cupertino.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:flutter_email_sender/flutter_email_sender.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:open_mail_app/open_mail_app.dart';
  10. import 'package:package_info_plus/package_info_plus.dart';
  11. import 'package:path_provider/path_provider.dart';
  12. import 'package:photos/core/configuration.dart';
  13. import 'package:photos/core/error-reporting/super_logging.dart';
  14. import 'package:photos/ente_theme_data.dart';
  15. import 'package:photos/ui/common/dialogs.dart';
  16. import 'package:photos/ui/tools/debug/log_file_viewer.dart';
  17. import 'package:photos/utils/dialog_util.dart';
  18. import 'package:photos/utils/toast_util.dart';
  19. import 'package:share_plus/share_plus.dart';
  20. import 'package:url_launcher/url_launcher.dart';
  21. final Logger _logger = Logger('email_util');
  22. bool isValidEmail(String? email) {
  23. if (email == null) {
  24. return false;
  25. }
  26. return EmailValidator.validate(email);
  27. }
  28. Future<void> sendLogs(
  29. BuildContext context,
  30. String title,
  31. String toEmail, {
  32. Function? postShare,
  33. String? subject,
  34. String? body,
  35. }) async {
  36. final List<Widget> actions = [
  37. TextButton(
  38. child: Row(
  39. mainAxisAlignment: MainAxisAlignment.start,
  40. children: [
  41. Icon(
  42. Icons.feed_outlined,
  43. color: Theme.of(context).iconTheme.color!.withOpacity(0.85),
  44. ),
  45. const Padding(padding: EdgeInsets.all(4)),
  46. Text(
  47. "View logs",
  48. style: TextStyle(
  49. color: Theme.of(context)
  50. .colorScheme
  51. .defaultTextColor
  52. .withOpacity(0.85),
  53. ),
  54. ),
  55. ],
  56. ),
  57. onPressed: () async {
  58. showDialog(
  59. context: context,
  60. builder: (BuildContext context) {
  61. return LogFileViewer(SuperLogging.logFile!);
  62. },
  63. barrierColor: Colors.black87,
  64. barrierDismissible: false,
  65. );
  66. },
  67. ),
  68. TextButton(
  69. child: Text(
  70. title,
  71. style: TextStyle(
  72. color: Theme.of(context).colorScheme.greenAlternative,
  73. ),
  74. ),
  75. onPressed: () async {
  76. Navigator.of(context, rootNavigator: true).pop('dialog');
  77. await _sendLogs(context, toEmail, subject, body);
  78. if (postShare != null) {
  79. postShare();
  80. }
  81. },
  82. ),
  83. ];
  84. final List<Widget> content = [];
  85. content.addAll(
  86. [
  87. const Text(
  88. "This will send across logs to help us debug your issue. Please note that file names will be included to help track issues with specific files.",
  89. style: TextStyle(
  90. height: 1.5,
  91. fontSize: 16,
  92. ),
  93. ),
  94. const Padding(padding: EdgeInsets.all(12)),
  95. Row(
  96. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  97. children: actions,
  98. ),
  99. ],
  100. );
  101. final confirmation = AlertDialog(
  102. title: Text(
  103. title,
  104. style: const TextStyle(
  105. fontSize: 18,
  106. ),
  107. ),
  108. content: SingleChildScrollView(
  109. child: ListBody(
  110. children: content,
  111. ),
  112. ),
  113. );
  114. showDialog(
  115. context: context,
  116. builder: (_) {
  117. return confirmation;
  118. },
  119. );
  120. }
  121. Future<void> _sendLogs(
  122. BuildContext context,
  123. String toEmail,
  124. String? subject,
  125. String? body,
  126. ) async {
  127. final String zipFilePath = await getZippedLogsFile(context);
  128. final Email email = Email(
  129. recipients: [toEmail],
  130. subject: subject ?? '',
  131. body: body ?? '',
  132. attachmentPaths: [zipFilePath],
  133. isHTML: false,
  134. );
  135. try {
  136. await FlutterEmailSender.send(email);
  137. } catch (e, s) {
  138. _logger.severe('email sender failed', e, s);
  139. await shareLogs(context, toEmail, zipFilePath);
  140. }
  141. }
  142. Future<String> getZippedLogsFile(BuildContext context) async {
  143. final dialog = createProgressDialog(context, "Preparing logs...");
  144. await dialog.show();
  145. final logsPath = (await getApplicationSupportDirectory()).path;
  146. final logsDirectory = Directory(logsPath + "/logs");
  147. final tempPath = (await getTemporaryDirectory()).path;
  148. final zipFilePath =
  149. tempPath + "/logs-${Configuration.instance.getUserID() ?? 0}.zip";
  150. final encoder = ZipFileEncoder();
  151. encoder.create(zipFilePath);
  152. encoder.addDirectory(logsDirectory);
  153. encoder.close();
  154. await dialog.hide();
  155. return zipFilePath;
  156. }
  157. Future<void> shareLogs(
  158. BuildContext context,
  159. String toEmail,
  160. String zipFilePath,
  161. ) async {
  162. final result = await showChoiceDialog(
  163. context,
  164. "Email logs",
  165. "Please send the logs to $toEmail",
  166. firstAction: "Copy email",
  167. secondAction: "Export logs",
  168. );
  169. if (result != null && result == DialogUserChoice.firstChoice) {
  170. await Clipboard.setData(ClipboardData(text: toEmail));
  171. }
  172. final Size size = MediaQuery.of(context).size;
  173. await Share.shareFiles(
  174. [zipFilePath],
  175. sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2),
  176. );
  177. }
  178. Future<void> sendEmail(
  179. BuildContext context, {
  180. required String to,
  181. String? subject,
  182. String? body,
  183. }) async {
  184. try {
  185. final String clientDebugInfo = await _clientInfo();
  186. final EmailContent emailContent = EmailContent(
  187. to: [
  188. to,
  189. ],
  190. subject: subject ?? '[Support]',
  191. body: (body ?? '') + clientDebugInfo,
  192. );
  193. if (Platform.isAndroid) {
  194. // Special handling due to issue in proton mail android client
  195. // https://github.com/ente-io/photos-app/pull/253
  196. final Uri params = Uri(
  197. scheme: 'mailto',
  198. path: to,
  199. query: 'subject=${emailContent.subject}&body=${emailContent.body}',
  200. );
  201. if (await canLaunchUrl(params)) {
  202. await launchUrl(params);
  203. } else {
  204. // this will trigger _showNoMailAppsDialog
  205. throw Exception('Could not launch ${params.toString()}');
  206. }
  207. } else {
  208. final OpenMailAppResult result =
  209. await OpenMailApp.composeNewEmailInMailApp(
  210. nativePickerTitle: 'Select emailContent app',
  211. emailContent: emailContent,
  212. );
  213. if (!result.didOpen && !result.canOpen) {
  214. _showNoMailAppsDialog(context, to);
  215. } else if (!result.didOpen && result.canOpen) {
  216. await showCupertinoModalPopup(
  217. context: context,
  218. builder: (_) => CupertinoActionSheet(
  219. title: Text("Select mail app \n $to"),
  220. actions: [
  221. for (var app in result.options)
  222. CupertinoActionSheetAction(
  223. child: Text(app.name),
  224. onPressed: () {
  225. final content = emailContent;
  226. OpenMailApp.composeNewEmailInSpecificMailApp(
  227. mailApp: app,
  228. emailContent: content,
  229. );
  230. Navigator.of(context, rootNavigator: true).pop();
  231. },
  232. ),
  233. ],
  234. cancelButton: CupertinoActionSheetAction(
  235. child: const Text("Cancel"),
  236. onPressed: () {
  237. Navigator.of(context, rootNavigator: true).pop();
  238. },
  239. ),
  240. ),
  241. );
  242. }
  243. }
  244. } catch (e) {
  245. _logger.severe("Failed to send emailContent to $to", e);
  246. _showNoMailAppsDialog(context, to);
  247. }
  248. }
  249. Future<String> _clientInfo() async {
  250. final packageInfo = await PackageInfo.fromPlatform();
  251. final String debugInfo =
  252. '\n\n\n\n ------------------- \nFollowing information can '
  253. 'help us in debugging if you are facing any issue '
  254. '\nRegistered email: ${Configuration.instance.getEmail()}'
  255. '\nClient: ${packageInfo.packageName}'
  256. '\nVersion : ${packageInfo.version}';
  257. return debugInfo;
  258. }
  259. void _showNoMailAppsDialog(BuildContext context, String toEmail) {
  260. showNewChoiceDialog(
  261. icon: Icons.email_outlined,
  262. context: context,
  263. title: 'Please email us at $toEmail',
  264. firstButtonLabel: "Copy email address",
  265. secondButtonLabel: "Dismiss",
  266. firstButtonOnTap: () async {
  267. await Clipboard.setData(ClipboardData(text: toEmail));
  268. showShortToast(context, 'Copied');
  269. },
  270. );
  271. }