Android でメールを送信する

概要

メールクライアントアプリを使わずに、自身が開発したアプリでメールを送信する方法です。Google のサービスを使ってメールを送信します。1つの方法は GmailSMTP を使い、もう1つの方法は Google APIGmail サービスを使います。

目次

確認環境

  • macOS 10.13
  • AndroidStudio 3.2.1
  • Gradle 4.6
  • Kotlin 1.3.0

参考情報

解説

認証方法の比較

GmailSMTP を使う方法と Google APIGmail サービスを使う方法の両方ともに、認証が必要になります。認証方法には3つの方法があります。以下のとおりです。

  • GmailSMTP を使う場合
    • Gmail アカウントのパスワードで認証する
    • アプリパスワードで認証する
      • 2段階認証を有効にしている場合
  • Google APIGmail サービスを使う場合
    • Android の機能で認証して、OAuth2 で認可する

GmailSMTP をパスワード認証で使う(非推奨)

この方法は実装方法が最も簡潔な方法です。しかし、使わない方が良い方法です。なぜなら、ユーザーのセキュリティリスクを高めてしまうためです。詳しくは後述します。

まず、app モジュールの build.gradle に JavaMail for Android への依存を追加します。

dependencies {
    //...
    implementation 'com.sun.mail:android-mail:1.5.5'
    implementation 'com.sun.mail:android-activation:1.5.5'
}

AndroidManifest.xml に INTERNET の使用権限を追加します。

    <uses-permission android:name="android.permission.INTERNET"/>

そして、メール送信を行う処理です。通信は Main (UI) スレッドで行えないため、AsyncTask を使っています。

package com.example.sendmail

import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
import android.os.AsyncTask
import java.util.*
import javax.mail.*

class SendMail(
    private val fromAddress: String,
    private val password: String,
    private val toAddress: String,
    private val subject: String,
    private val text: String
) : AsyncTask<Unit, Unit, Unit>() {

    override fun doInBackground(vararg params: Unit?) {
        val props = Properties().apply {
            putAll(mapOf(
                "mail.smtp.host" to "smtp.gmail.com",
                "mail.smtp.socketFactory.port" to "465",
                "mail.smtp.socketFactory.class" to "javax.net.ssl.SSLSocketFactory",
                "mail.smtp.auth" to "true",
                "mail.smtp.port" to "465"
            ))
        }

        val session = Session.getDefaultInstance(props, object : Authenticator() {
            override fun getPasswordAuthentication() = PasswordAuthentication(fromAddress, password)
        })

        val message = MimeMessage(session).also {
            it.setFrom(InternetAddress(fromAddress))
            it.addRecipient(Message.RecipientType.TO, InternetAddress(toAddress))
            it.subject = subject
            it.setText(text)
        }

        Transport.send(message)
    }
}

Kotlin の文法に詳しくない方のための補足説明:
クラス名直後の () はコンストラクタの引数です。val と指定されているため、クラスのプロパティでもあります、プロパティは、フィールドと getter/setter を合わせたものです。Java におけるフィールドの定義と this.fromAddress = fromAddress; の様なコンストラクタの引数をフィールドに代入する処理を行っています。Java ではフィールド定義、コンストラクタ定義と分かれているところを Kotlin では1つにまとめて書けます。: AsyncTask で AsyncTask クラスを継承しています。継承すると共に、AsyncTask<...>() として AsyncTask クラスの引数なしコンストラクタを SendMail オブジェクトの生成時に呼び出しています。変数 props には Properties オブジェクトが代入されます。但し、Properties オブジェクトには apply 関数の {} 内の処理が適用されます。object : Authenticator() では Authenticator クラスを継承した無名クラスのオブジェクトを生成しています。MimeMessege(session).alsoMimeMessege(session).apply とほぼ同じなのですが、apply の場合は this.subject = subject となり、also の場合は it.subject = subject になります。apply の場合は subjectthis.subject が同じ変数を指してしまうため、ここでは also を使っています。詳しくは Reference - Kotlin Programming Language を参照してください。


最後に SendMail の呼び出しです。適宜、引数の内容は書き換えてください。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val task = SendMail(
            fromAddress = "your_gmail_account@gmail.com",
            password = "<your password>",
            toAddress = "your_friend_account@example.com",
            subject = "First Practice",
            text = "Hello World!"
        )
        task.execute()
    }
}

以上で完成です。アプリを実行させてみると、残念ながら LogCat に以下のエラーが表示されます。

javax.mail.AuthenticationFailedException: 534-5.7.14

エラーメッセージには https://support.google.com/mail/answer/78754 を見るように書かれています。読んでみると、安全性の低いアプリを許可する必要があることが分かります。許可は https://myaccount.google.com/lesssecureapps で行えます。有効に設定した上で、再びアプリを実行してみましょう。今度はメールが送信され、指定のアドレスに届いているはずです。届いていない場合は、パスワードが間違えていないか確認しましょう。

しかし、安全性の低いアプリの許可を有効にすると、様々な怪しいアプリにも許可を与えることになります。これは危険です。先程のヘルプには、2段階認証を設定し、アプリパスワードを使う方法があると書かれています。この方法は使わずに、アプリパスワードを使う方法を使います。先に進む前に、安全性の低いアプリの許可は無効に戻しておきましょう。

GmailSMTP をアプリパスワードで使う

では、2段階認証を有効にし、アプリパスワードを使ってメールを送信してみます。

https://myaccount.google.com/lesssecureapps のページの左向き矢印を選択すると、ログインとセキュリティの設定ページに移動します。パスワードとログイン方法の項目に、2段階認証プロセスの項目があります。手順に従って有効にしましょう。

手順どおりに進めて、次の画面が表示されたら2段階認証プロセスの設定は完了です。

左向き矢印を選択して、メニュー階層を戻りましょう。2段階認証プロセスを有効にすると、アプリパスワードの項目が追加されます。

アプリパスワードを追加します。メールを選択し、端末は Android が見当たらないのでその他を選択します。端末名の入力では Android とでも入力しておきます。

生成を行うと、黄色い枠のなかにアプリパスワードが表示されます。(黒塗りして隠しています。)

このアプリパスワードをコピーし、Gmail アカウントのパスワードと差し替えます。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val task = SendMail(
            fromAddress = "your_gmail_account@gmail.com",
            password = "<your application password>", // 差し替え
            toAddress = "your_friend_account@example.com",
            subject = "First Practice",
            text = "Hello World!"
        )
        task.execute()
    }
}

アプリを実行してみましょう。メールが送信され、指定のアドレスに届いているはずです。

Google APIGmail サービスを OAuth2 で使う

アプリパスワードを使う方法では、ユーザーにアプリパスワードの設定を行ってもらう必要があります。限られた環境のみで使うのであれば、手軽な方法ではあります。多くのユーザーが使うことを想定すると、使い勝手の観点から現実的ではありません。そこで、Android の機能で認証を行い、Google APIGmail サービスを OAuth2 で認可する仕組みを使います。

まずは、app モジュールの build.gradle にライブラリへの依存を追加します。

dependencies {
    // ...
    implementation 'com.sun.mail:android-mail:1.5.5'

    implementation('com.google.android.gms:play-services-auth:16.0.1') {
        exclude group: 'com.android.support'
    }
    implementation 'com.google.api-client:google-api-client-android:1.25.0'
    implementation 'com.google.api-client:google-api-client-gson:1.25.0'
    implementation 'com.google.apis:google-api-services-gmail:v1-rev96-1.25.0'
}

android-mail は無くても構いませんが、使うと Message (後述) の構築が楽です。play-services-auth では com.android.support を除外しています。Support Library への依存を別途追加する場合に、バージョンの違いでエラーになるため除外しています。

AndroidManifest.xml には INTERNET の使用権限を追加します。

    <uses-permission android:name="android.permission.INTERNET"/>

そして、メール送信を行う処理です。通信は Main (UI) スレッドで行えないため、ここでも AsyncTask を使っています。

package com.example.sendmail

import android.content.Intent
import android.os.AsyncTask
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.google.api.client.json.gson.GsonFactory
import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64
import com.google.api.services.gmail.Gmail
import com.google.api.services.gmail.model.Message
import java.io.ByteArrayOutputStream
import java.util.*
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage

class SendMailUsingGmailService(
    private val credential: GoogleAccountCredential,
    private val message: MimeMessage,
    private val onAuthError: (Intent) -> Unit
) : AsyncTask<Unit, Unit, Intent?>() {

    companion object {
        private fun createMimeMessage(
            fromAddress: String,
            toAddress: String,
            subject: String,
            text: String
        ): MimeMessage {
            val session = Session.getDefaultInstance(Properties())
            return MimeMessage(session).also {
                it.setFrom(InternetAddress(fromAddress))
                it.addRecipient(javax.mail.Message.RecipientType.TO, InternetAddress(toAddress))
                it.subject = subject
                it.setText(text)
            }
        }
    }

    constructor(
        credential: GoogleAccountCredential,
        toAddress: String,
        subject: String,
        text: String,
        onAuthError: (Intent) -> Unit
    ) : this(
        credential,
        createMimeMessage(credential.selectedAccountName, toAddress, subject, text),
        onAuthError
    )

    override fun onPostExecute(result: Intent?) {
        result?.also(onAuthError)
    }

    override fun doInBackground(vararg params: Unit?): Intent? {
        return try {
            buildService(credential).send(message)
            null
        } catch (e: UserRecoverableAuthIOException) {
            e.intent
        }
    }

    private fun buildService(credential: GoogleAccountCredential): Gmail {
        val transport = AndroidHttp.newCompatibleTransport()
        return Gmail.Builder(transport, GsonFactory(), credential)
            .setApplicationName("Send Mail")
            .build()
    }

    private fun Gmail.send(mimeMessage: MimeMessage) {
        users()
            .messages()
            .send(credential.selectedAccountName, createMessage(mimeMessage))
            .execute()
    }

    private fun createMessage(mimeMessage: MimeMessage): Message {
        return Message().apply {
            val bytes = ByteArrayOutputStream()
                .also(mimeMessage::writeTo)
                .toByteArray()
            raw = Base64.encodeBase64URLSafeString(bytes)
        }
    }
}

Kotlin の文法に詳しくない方のための補足説明:
クラス名直後の () はプライマリコンストラクタの引数です。SendMailUsingGmailService クラスには2つのコンストラクタがあります。constructor で始まる部分はセカンダリコンストラクタと呼ばれ、this(...) でプライマリコンストラクタを呼び出しています。companion object の {} 内に関数を書くとクラスメソッドとして振る舞います。result?.also(onAuthError) は、result が null では無いときのみ onAuthError(result) を実行する書き方です。? を書かない場合は null チェックを行わずに onAuthError(result) を実行します。try-catch 構文は try, catch のブロック内の最後の値が try-catch 式の値となって、その値が return により関数の戻り値にされます。具体的には、UserRecoverableAuthIOException が発生した場合は e.intent が戻り値となり、成功した場合は null が戻り値になります。fun Gmail.send(...) は拡張関数と呼ばれ、Gmail クラスに send メソッドが追加された様に見せかけることができます。そのため、buildService(credential).send(message) という書き方ができています。buildService の戻り値は Gmail オブジェクトですので、拡張関数である send メソッド(関数)を呼び出せる様になっています。 詳しくは Reference - Kotlin Programming Language を参照してください。


さて、本題に戻って処理の内容です。Google アカウントで認証した結果が GoogleAccountCredential として渡されます。メールのコンテンツは MimeMessage として扱われます。buildService 関数では Gmail サービスを準備し、send 関数でサービスを利用しています。createMessage 関数では MimeMessage をバイト列に変換して、 API に送信するメッセージ本文の raw にそのバイト列を設定しています。

SendMailUsingGmailService の呼び出しは Google アカウントを使った認証と OAuth2 による認可の手順が必要になるため複雑になります。

package com.example.sendmail

import android.accounts.AccountManager
import android.app.Activity
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.gmail.GmailScopes

class MainActivity : AppCompatActivity() {

    companion object {
        private const val REQUEST_ACCOUNT_CHOOSER = 1
        private const val REQUEST_AUTHORIZATION = 2
    }

    private lateinit var mCredential: GoogleAccountCredential

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mCredential = GoogleAccountCredential.usingOAuth2(this, listOf(GmailScopes.GMAIL_SEND))
        startActivityForResult(mCredential.newChooseAccountIntent(), REQUEST_ACCOUNT_CHOOSER)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
            REQUEST_ACCOUNT_CHOOSER -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
                    Log.i(localClassName, "Account Name: $accountName")
                    if (accountName != null) {
                        mCredential.selectedAccountName = accountName
                    }
                    sendMail()
                }
            }
            REQUEST_AUTHORIZATION -> sendMail()
        }
    }

    private fun sendMail() {
        val task = SendMailUsingGmailService(
            mCredential,
            toAddress = "your_friend_account@example.com",
            subject = "First Practice",
            text = "Hello World!"
        ) {
            startActivityForResult(it, REQUEST_AUTHORIZATION)
        }

        task.execute()
    }
}

Kotlin の文法に詳しくない方のための補足説明:
val は再代入不可な変数、var は再代入可能な変数です。lateinit は後で初期化することを示す印です。lateinit を使わない場合には var mCredential: GoogleAccountCredential? = null とします。変数の型に ? が付く場合には、変数に null の代入を許容することになるので変数を使う時に null チェックが必要になります。変数を使う前に必ず null 以外で初期化され、変数を使う時の null チェックを減らしたい場合に lateinit を使います。SendMailUsingGmailService のコンストラクタの引数には onAuthError が必要です。しかし、一見すると () 内には引数が見当たりません。Kotlin では、最後の引数の型が関数である場合 () の外側に記述できます。{} はブロック(複数の文をまとめる構造)、もしくはラムダ式(関数の一種)を表しており、{ startActivityForResult(...) } は関数の一種ということになります。onAuthError 引数にはこの関数が代入されます。詳しくは Reference - Kotlin Programming Language を参照してください。


GmailScopes.GMAIL_SEND を指定して、送信サービスの認可を得ようとしています。startActivityForResult(..., REQUEST_ACCOUNT_CHOOSER) ではアカウントの選択を起動します。アカウントを選択後 onActivityResult の REQUEST_ACCOUNT_CHOOSER の部分が実行されます。選択されたアカウント名を GoogleAccountCredential に設定しています。これにより GoogleAccountCredential が整ったので、メールの送信処理に移ります。メールを送信しようとすると、サービスへのアクセス許可が与えられていない場合に onAuthError コールバックが呼び出されます。コールバックでは startActivityForResult(it, REQUEST_AUTHORIZATION) を呼び出してアクセス許可を与えるかの選択をユーザーに促します。選択の後、onActivityResult の REQUEST_AUTHORIZATION の部分が実行されます。再びメールを送信し、アクセス許可が与えられていたら onAuthError コールバックは呼び出されずに終了します。

以上で、実装は完了しました。アプリを起動してみます。しかし、ビルドの途中で以下のエラーが表示されるかもしれません。

More than one file was found with OS independent path 'META-INF/DEPENDENCIES'

この場合には、build.gradle に以下を追加すれば解決します。(理由は調べられていません。どなたか教えて頂けると嬉しいです。)

android {
    // ...
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
    }
}

アプリが起動するとアカウント選択が表示され、いずれかのアカウントを選択します。

そうすると、エラーが発生します。LogCat に以下のエラーが表示されます。

com.google.android.gms.auth.GoogleAuthException: UNREGISTERED_ON_API_CONSOLE

解消するためには Developer Console でサービスを有効化しておく必要があります。

Developer Console でサービスを有効化

まず、Google Cloud Platform から Console を開きます。既存のプロジェクトでサービスを有効化しても良いですが、ここでは新しいプロジェクトを作成します。プロジェクト名は何でも良いので、ここでは SendMail にしておきます。

プロジェクト (SendMail) が選択されていることを確認した上で、API とサービスのメニューを開きます。ここから「API とサービスの有効化」を選択します

Gmail を検索し、Gmail API を選択します。

Gmail API を有効にします。

次に、Gmail API の認証情報を設定します。認証情報メニューを選択し、「認証情報を作成」を選択します。

使用する APIGmail APIAPI を呼び出す場所は Android、アクセスするデータの種類はユーザーデータです。

「必要な認証情報」を選択すると次のステップに進みます。

クライアント ID を作成するにあたっての名前は後で見て分かる名前を付けます。署名証明書フィンガープリントは Terminal で keytool コマンドを使って取得します。

書かれているとおりのコマンドを実行しますが、path-to-debug-or-production-keystore は自分で埋める必要があります。debug-keystore を使う場合は以下のとおりに実行します。(macOS の場合です。)

$ keytool -exportcert -keystore ~/.android/debug.keystore -list -v

パスワードを求められますが、何も入力せずに Enter で先に進めます。表示された情報のなかから、SHA1 のフィンガープリントをコピーします。Android アプリを識別するための情報(指紋)です。配布するアプリの場合には、自分で作成した production-keystore ファイルのパスを指定します。今回は実験用なので debug-keystore を使っています。

パッケージ名は AndroidManifest.xml で指定したパッケージ名を記入します。しかし、build.gradle の applicationId で異なるパッケージ名を設定している場合には applicationId を記入する必要があるようです。私は試していませんが StackOverflow でみかけました。なお、1つの Android アプリを複数のプロジェクトには設定できない様です。異なるアカウントの異なるプロジェクトに同一アプリを設定しようとしたところ、設定できませんでした。

記入を終えて「OAuth クライアント ID を作成」を選択すると次のステップに進みます。

次は、ユーザーに表示される同意画面の設定です。ユーザーに見せるサポート用のメールアドレス、サービス名を設定します。今回は API を使うサービスが Android のアプリだけなので Android アプリの名前を設定しています。サービス名が別途存在する場合には、そちらを設定すれば良いでしょう。

「次へ」を選択すると最後のステップに進みます。Android アプリで使う場合には、認証情報をダウンロードしなくて構いません。完了を選択します。

次に、OAuth 同意画面を設定します。アプリケーション名、サポートメールは先程設定した内容が表示されます。

下にスクロールすると Google API のスコープの設定が現れます。ユーザーにアクセスを許可する範囲の設定です。「スコープを追加」を選択し、gmail.send を探して追加します。

更に下にスクロールすると保存ボタンがあります。個人的な利用の範囲であれば保存で構いません。しかし、一般に公開したい場合には「確認のため送信」を選択する必要があります。公開の前に Google による確認が行われます。今回は省略していますが、プライバシーポリシーの設定などの他の項目も適切に設定した上で確認を依頼しましょう。

以上でサービスの有効化は完了しました。アプリを起動して試してみましょう。Google アカウントの選択までは既に行えていました。

アカウントを選択するとエラーではなく、同意画面が表示されます。

許可を選択することで同意したことになり、gmail.send (GmailScopes.GMAIL_SEND) が有効になります。これで、Gmail サービスを使ったメール送信が行える様になります。

おつかれさまでした。