【Flutter】zipファイルをメール送信

2024/04/03に公開

概要

flutter_email_senderで、件名・送信先・添付ファイル(zipファイル)を設定した上でメールアプリを起動し、メール送信を行います。

flutter_email_senderを使ってみた理由

他の方達もflutter_email_senderを使ってログデータをメール送信している記事を見かけたので、私もスマホをリモートで検証後にログデータをPCで簡単に解析したいなというのが理由です。簡単にできるかなと思っていましたが、やってみるとエラーが発生し思った以上に苦労しました。詳細については実装内容を参照して下さい。
https://zenn.dev/senkyaku/articles/b575a41a46e898

パッケージ

https://pub.dev/packages/flutter_email_sender

準備

  • Android端末(自分の端末はandroid13)
  • Mircrosoft Outlookアプリインストール、アカウント登録(Gmailだとセキュリティの関係でうまくいかないかも)

実装

  1. ターミナルで下記を実行
 flutter pub add flutter_email_sender path_provider archive image_picker
 flutter pub get

実装上はimage_pickerを使用していませんが、追加しないと下記コンパイルエラーが起きました。
flutter_email_senderのサンプルコードでもimage_pickerが入っているので、使用していなくてもflutter_email_senderを使う時は入れておいた方がよさそうでした。

image_pickerを入れていなかったときのエラー

FAILURE: Build failed with an exception.

  • What went wrong:
    Execution failed for task ':app:checkDebugDuplicateClasses'.

A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.internal.jdk8.JDK8PlatformImplementations found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.internal.jdk8.JDK8PlatformImplementations$ReflectSdkVersion found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.jvm.jdk8.JvmRepeatableKt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.jvm.optionals.OptionalsKt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.random.jdk8.PlatformThreadLocalRandom found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.streams.jdk8.StreamsKt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.streams.jdk8.StreamsKtasSequenceinlinedSequence$1 found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.streams.jdk8.StreamsKtasSequenceinlinedSequence$2 found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.streams.jdk8.StreamsKtasSequenceinlinedSequence$3 found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.streams.jdk8.StreamsKtasSequenceinlinedSequence$4 found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.text.jdk8.RegexExtensionsJDK8Kt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)
Duplicate class kotlin.time.jdk8.DurationConversionsJDK8Kt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)

上記のエラーは、kotlinのバージョンで怒られているみたいなのでbuild.gradleを下記の方法で修正してみるとコンパイルエラーは回避できるのですが、flutter_email_senderでファイルを添付しようとしたときにうまくいきませんでした。ですので、image_pickerを追加してコンパイルした方がよいみたいでした(ここら辺で時間を余計にかかってしまいました)。

build.gradleに追加したがうまくいかなかった(使用しないように)

configurations.all {
resolutionStrategy {
eachDependency {
if ((requested.group == "org.jetbrains.kotlin") && (requested.name.startsWith("kotlin-stdlib"))) {
useVersion("1.8.0")
}
}
}
}

  1. 実装
    Android11以降の端末を使用する場合、AndroidManifestに下記項目を追記する必要があります。
AndroidManifest.xml
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.SENDTO" />
+            <data android:scheme="mailto" />
+        </intent>
+    </queries>
    <application
        android:label="mail_sender_sample1"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
 </manifest>
main.dart
import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'メール送信サンプル',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MailSenderSample1(title: 'メール送信サンプル'),
    );
  }
}

class MailSenderSample1 extends StatefulWidget {
  const MailSenderSample1({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MailSenderSample1> createState() => _MailSenderSample1State();
}

class _MailSenderSample1State extends State<MailSenderSample1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                flutterEmailSenderMail();
              },
              child: const Text('メール送信'),
            ),
          ],
        ),
      ),
    );
  }

  flutterEmailSenderMail() async {
    try {
      final appDocumentDir = await getApplicationDocumentsDirectory();
      final filePath = appDocumentDir.path + '/file.txt';
      final file = File(filePath);
      await file.writeAsString('Text file in app directory');

      final zipFilePath = appDocumentDir.path + '/file.zip';
      if (await file.exists()) {
        //print("flutterEmailSenderMail() doing...");
        var encoder = ZipFileEncoder();
        encoder.create(zipFilePath);
        encoder.addFile(file);
        encoder.close();
      }

      final Email email = Email(
        body: 'ログ取得テスト用にメール送信。\n',
        subject: 'flutter_email_senderを使ってメールを送っています。',
        recipients: [
          'sample@example.co.jp',
        ],
        attachmentPaths: [
          zipFilePath,
        ],
      );
      await FlutterEmailSender.send(email);
    } catch (e) {
      print("error=${e}");
    }
  }
}
  1. 実行
    実行すると下記の画面が表示され、送信ボタンを押すとoutlookアプリ起動し、件名・送信先・添付ファイルが設定済みの送信画面が表示されるので送信します。

  2. 受信メール
    パソコン上で受信メールを確認するとちゃんと件名・本文・添付ファイルも問題なく受信できていました。

  3. メール設定

      final Email email = Email(
        body: 'ログ取得テスト用にメール送信。\n',
        subject: 'flutter_email_senderを使ってメールを送っています。',
        recipients: ['宛先@example.co.jp'],
        attachmentPaths: ['添付ファイル'],
      );
      await FlutterEmailSender.send(email);

おわりに

以上、zipファイルの送信でした。最後まで読んでもらいましてありがとうございます。
flutter_email_senderパッケージは他の方達も記事に乗せていたので、簡単にできるだろうと最初は思っていましたが、予期しないエラーが出たりして思った以上に時間がかかってしまいました。使ったことがないパッケージだとこういうことが起こるんだと実感しました。
とりあえず、ログデータをzip化してメール送信できるようになったので、【Flutter】loggingで自由な表示形式でログを保存するの記事と合わせると、ログ解析がやりやすくなって開発が進めやすくなりそうです。実際にお客さんにメールアプリをインストールしてアカウント登録をしてくれとかいう手間をかけてもらうわけにもいかないと思うので、実際にログを取ってくださいと言われたときはログ用のサーバ立ててhttp通信でアップロードするんだろうけど、検証段階でも開発がしやすくなるのは良いですね。

参考

https://pub.dev/packages/flutter_email_sender
https://pub.dev/packages/path_provider
https://pub.dev/packages/image_picker
https://pub.dev/packages/archive
https://zenn.dev/senkyaku/articles/b575a41a46e898

Discussion