email_util.dart 8.3 KB

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