SQLの窓

2017年01月27日


Android Studio で、javamail を gradle に書いて添付ファイル付きメール送信を行う

数年前までは、Google Code の 2009 年ライブラリを使用して Android 用のメール送信を行っていたのですが、

参照 ➡ javamail-android + AsyncTask でメール送信を行う為のテンプレート

今回改めて調べてみると、オリジナルの javamail 側で Android の対応がなされていました。gradle に記述するだけで使用可能です。

▼ 今回使用したもの
apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "23.0.3"
    defaultConfig {
        applicationId "january17.lightbox.mailapplication"
        minSdkVersion 19
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    packagingOptions {
        pickFirst 'META-INF/LICENSE.txt' // picks the JavaMail license file
    }
}

repositories {
    jcenter()
    maven {
        url "https://maven.java.net/content/groups/public/"
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.sun.mail:android-mail:1.5.5'
    compile 'com.sun.mail:android-activation:1.5.5'
    testCompile 'junit:junit:4.12'
}


さらに、今回は添付ファイルを付加する為に調べてみると、MimeBodyPart クラスの attachFile メソッドに、ファイルのパスを渡すだけで全てライブラリが実行してくれる事が解りました。さっそくオリジナルのソースコードを読んでみると、古くからあるサンプルの FileDataSource を使った内容(Google code バージョンでも同等の処理をしていました)であり、さらに Google code バージョンには無い contentType と encoding を指定できる attachFile メソッドも存在していました。

添付ファイル用 AndroidSendmail クラス
public class AndroidSendmail {

	private String server;
	private String port;
	private String userid;
	private String password;
	private String username;

	// ************************************
	// コンストラクタ
	// ************************************
	public AndroidSendmail(
		String server,
		String port,
		String userid,
		String password,
		String username) {

		this.server = server;
		this.port = port;
		this.userid = userid;
		this.password = password;
		this.username = username;
	}

	// ************************************
	// AsyncTask の onPostExecute から外部イベントとして
	// 呼び出す為のインターフェイス
	// ************************************
	public interface SendMailed {
		public void onMailResult(String result);
	}

	// ************************************
	// メール送信
	// ************************************
	public void SendMail(String to, String from, String subject, String body,String file, final SendMailed sm) {

		new AsyncTask<String, Void, String>() {

			// ************************************
			// 非同期処理
			// ************************************
			@Override
			protected String doInBackground(String... params) {

				Log.i("lightbox","開始");

				String result_string = "";
				try {
					// ************************************
					// プロパティオブジェクトを作成
					// プロパティオブジェクトは、extends Hashtable(連想配列)
					// ************************************
					Properties props = new Properties();

					// ************************************
					// * 連想配列に送信用サーバのアドレスをセット
					// ************************************
//					props.put("mail.smtp.host","smtp.mail.yahoo.co.jp");	// Yahoo
//					props.put("mail.smtp.host","smtp.live.com");	// Microsoft
					props.put("mail.smtp.host", server);
					props.put("mail.smtp.port", port);
					props.put("mail.smtp.auth", "true" );	// SMTP 認証を行う

					// ************************************
					// SSL関連設定
					// ************************************
					if ( port.equals("465") ) {
						props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
						props.put("mail.smtp.socketFactory.fallback", "false");
						props.put("mail.smtp.socketFactory.port", port);
					}

					// ************************************
					// 暗号化
					// ************************************
					if ( port.equals("587") ) {
						props.put("mail.smtp.starttls.enable", "true");
					}


					// ************************************
					// メール用のセッションを作成
					// ************************************
					SimpleAuthenticator sa =
						new SimpleAuthenticator(userid, password);
					Session MailSession =
						Session.getInstance( props, sa );

					// ************************************
					// メール用のメッセージオブジェクトを作成
					// ************************************
					MimeMessage msg = new MimeMessage(MailSession);

					// ************************************
					// 宛先
					// ( 第一引数では、CC や BCC を指定できます。)
					// ************************************
					msg.setRecipients(
						Message.RecipientType.TO,
						new Address[] { new InternetAddress( params[0], "日本語相手先", "ISO-2022-JP" ) }
					);

					// ************************************
					// 送信者
					// ************************************
					msg.setFrom(
						new InternetAddress( params[1], username, "ISO-2022-JP" )
					);

					// ************************************
					// 件名
					// ************************************
					msg.setSubject( params[2], "ISO-2022-JP" );

					// ************************************
					// 本文(テキスト)
					// ************************************
					MimeBodyPart mbp1 = new MimeBodyPart();
					mbp1.setText(params[3], "ISO-2022-JP");

					// ************************************
					// 添付ファイル
					// ************************************
					MimeBodyPart mbp2 = new MimeBodyPart();
					mbp2.attachFile(params[4]);

					// ************************************
					// マルチパート作成
					// ************************************
					Multipart mp = new MimeMultipart();
					mp.addBodyPart(mbp1);
					mp.addBodyPart(mbp2);

					msg.setContent(mp);

					// ************************************
					// 送信
					// ************************************
					Transport.send( msg );

					result_string = "メールの送信を完了しました";

				}
				catch( Exception e ) {
					e.printStackTrace();
					result_string = "メールの送信に失敗しました";
				}

				Log.i("lightbox","終了");

				return result_string;
			}

			// ************************************
			// 非同期処理終了後の処理( 画面へのアクセスが可能 )
			// ************************************
			@Override
			protected void onPostExecute(String result) {
				// 引数のインターフェイス内のメソッドを呼び出す
				sm.onMailResult(result);
			}

		}.execute(to, from, subject, body,file);

	}

	// ************************************
	// 認証用のプライベートクラス
	// ************************************
	private class SimpleAuthenticator extends Authenticator {

		private String user_string = null;
		private String pass_string = null;

		public SimpleAuthenticator( String user_s, String pass_s ) {
			super();
			user_string = user_s;
			pass_string = pass_s;
		}

		protected PasswordAuthentication getPasswordAuthentication(){
			return new PasswordAuthentication( this.user_string, this.pass_string );
		}
	}
}

添付するファイルは、ギャラリーから取得する画像を使ってテストしました。昨今、Android では、ギャラリーから取得する為の intent のパラメータが過去バージョンから変わっており、取得する uri も 直接のパスでは無い為、作法にのっとってデーターベースより必要な情報を収集しています。

MainActivity

※ ここでは、MIME と 画像の表示名を DB から取得していますが、使用していません
※ Gmail を使用する場合は、安全性の低いアプリの許可を『有効』にする 必要があります。
public class MainActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Button button = (Button) MainActivity.this.findViewById(R.id.button);
		button.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {

				// ギャラリーの呼び出し
				Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
				intent.addCategory(Intent.CATEGORY_OPENABLE);
				intent.setType("image/*");
				startActivityForResult(intent, 10);        // 10 は任意

			}
		});
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);

		if (requestCode == 10 && resultCode == Activity.RESULT_OK) {
			if (data != null) {
				Uri uri = data.getData();

				String id = DocumentsContract.getDocumentId(uri);
				Log.i("lightbox", "id文字列:" + id);

				// カラムリストの指定
				String[] proj = {
					MediaStore.Images.Media.DATA,
					MediaStore.Images.Media.MIME_TYPE,
					MediaStore.Images.Media.DISPLAY_NAME
				};
				String selection = "_id=?";
				String[] args = new String[]{id.split(":")[1]};

				// 目的データのカーソルを取得
				ContentResolver contentResolver = getContentResolver();
				Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj, selection, args, null);
				cursor.moveToFirst();    // 読み出し

				// データを取得
				String result = cursor.getString(0);
				String mime = cursor.getString(1);
				String name = cursor.getString(2);
				Log.i("lightbox", "Path:" + result);
				cursor.close();


				AndroidSendmail as = new AndroidSendmail(
					"smtp.gmail.com", // サーバ( 例 : "smtp.mail.yahoo.co.jp" or "smtp.live.com" )
					"465", // 465 または 587
					"アカウント", // アカウント
					"パスワード", // パスワード
					"日本語ユーザ名"
				);

				as.SendMail(
					"宛先メールアドレス",
					"自分のメールアドレス",
					"件名",
					"本文一行目\n本文二行目",
					result, // ファイルへのパス
					new AndroidSendmail.SendMailed() {
						@Override
						public void onMailResult(String result) {
							Log.i("lightbox", result);
						}
					}
				);

			}
		}
	}
}


上のコードでは、Android API 19 以上で利用できる、getDocumentId を使用していますが、それより古いバージョンでは 呼び出し時に ACTION_GET_CONTENT を使用して、取得できる uri の パスの一番最後の数値を _id に使用して取得できました。
※ API 18(4.3.1) と API 15(4.0.3) を エミュレータで確認しました。
Uri uri = data.getData();

Log.i("lightbox", "id文字列:" + uri.toString());

// カラムリストの指定
String[] proj = {
	MediaStore.Images.Media.DATA,
	MediaStore.Images.Media.MIME_TYPE,
	MediaStore.Images.Media.DISPLAY_NAME
};
String selection = "_id=?";
String[] path = (uri.toString()).split("/");
String[] args = new String[]{path[path.length-1]};


Manifest.xml

※ INTERNET と READ_EXTERNAL_STORAGE が必要です
<?xml version="1.0" encoding="utf-8"?>
<manifest package="january17.lightbox.mailapplication"
          xmlns:android="http://schemas.android.com/apk/res/android">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

最後に

マルチパートの内容を WEB上の Outlook から送って比べてみました。Microsoft は、異常に多くの情報を持っていましたが、一般的なデータは javamail の setText(本文, "ISO-2022-JP") と attachFile(ファイルのパス) で問題無いようでした。(ヘッダ部分は通常の WEB メールで確認できます)
▼ Microsoft()
--_002_PS1PR01MB0667CE2FF5CECC981B23A8CF81710PS1PR01MB0667apcp_
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: quoted-printable

=1B$BK\J80l9TL\=1B(B
=1B$BK\J8Fs9TL\=1B(B

--_002_PS1PR01MB0667CE2FF5CECC981B23A8CF81710PS1PR01MB0667apcp_
Content-Type: image/jpeg; name="2013-01-26T01-26-00_0.jpg"
Content-Description: 2013-01-26T01-26-00_0.jpg
Content-Disposition: attachment; filename="2013-01-26T01-26-00_0.jpg";
	size=431072; creation-date="Fri, 20 Jan 2017 16:29:33 GMT";
	modification-date="Fri, 20 Jan 2017 16:29:33 GMT"
Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAQAAAQABAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQA
AAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAGYKADAAQAAAABAAAEyAAAAAD/2wBD
AAIBAQIBAQICAQICAgICAwUDAwMDAwYEBAMFBwYHBwcGBgYHCAsJBwgKCAYGCQ0JCgsLDAwMBwkN

▼ JavaMail for Android
------=_Part_0_1107074864.1484932123775
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: quoted-printable

=1B$BK\J80l9TL\=1B(B
=1B$BK\J8Fs9TL\=1B(B

------=_Part_0_1107074864.1484932123775
Content-Type: image/jpeg; name=KIMG0178.JPG
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=KIMG0178.JPG

/9j/4TjERXhpZgAATU0AKgAAAAgACgEPAAIAAAAIAAAAhgEQAAIAAAAGAAAAjgESAAMAAAABAAMA
AAEaAAUAAAABAAAAlAEbAAUAAAABAAAAnAEoAAMAAAABAAIAAAExAAIAAAALAAAApAEyAAIAAAAU
AAAAsAITAAMAAAABAAEAAIdpAAQAAAABAAAAxAAABEpLWU9DRVJBADQwNEtDAAAAAEgAAAABAAAA



posted by lightbox at 2017-01-27 21:29 | 2017 Android Studio | このブログの読者になる | 更新情報をチェックする

2017年01月23日


Android 内の画像データの一覧を CSV に出力する

出力場所として、USB 参照できる場所にフォルダを作成してその中に CSV ファイルを書き込むようにします。おそらく最新の CSV を USB 経由で確認するには、Android 本体を再起動する必要があると思いますが、パッケージ内部に作成するといろいろ面倒なので、こうした上で Android Device Monitor から pull で 取り出すのがいいと思います。
	private String okhttp_folder;
	private String[] columnNames;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);


		// USB から参照可能な場所にフォルダを作成
		// 但し、更新直後のデータのアクセスは、Device Monitor
		// USB からの場合は Device を再起動
		okhttp_folder = Environment.getExternalStorageDirectory().getPath() + "/okhttp";
		File file = new File(okhttp_folder);
		// ディレクトリ初期作成
		if (!file.exists()) {
			if (file.mkdir() == false) {
				Log.i("lightbox", "ディレクトリを作成できませんでした");
			}
		}

		Button button = (Button) MainActivity.this.findViewById(R.id.button);
		button.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				writeCsv("image_data.csv");
			}
		});
	}


CSV ファイルを作成しやすいよう(カンマの付加を単純にする為)に、固定で row_no を先頭に追加しています。最初に列名リストを取得していますが、この際、カーソルを取得するだけで取り出し可能です。データ部分は moveToFirst と moveToNext が必要ですが、一行で実行するために三項演算子を使用しています(初回のみ null で以降 true か false)。

結果の内容を Excel で開いた様子
	private void writeCsv( String filename ) {

		ContentResolver contentResolver = getContentResolver();
		Uri listing = MediaStore.Images.Media.getContentUri("external");
		Log.i("lightbox", "Content URI : " + listing.toString());
		// ▲ content://media/external/images/media
		Cursor cursor;

		cursor	= contentResolver.query(listing, null, null, null, null);

		// 行バッファ
		StringBuilder builder = new StringBuilder();

		// 列名一覧
		String[] columnNames = cursor.getColumnNames();
/*
		_id
		_data
		_size
		_display_name
		mime_type
		title
		date_added
		date_modified
		description
		picasa_id
		isprivate
		latitude
		longitude
		datetaken
		orientation
		mini_thumb_magic
		bucket_id
		bucket_display_name
		width
		height
*/

		// CSV データを作成
		builder.append("row_no");
		for ( int i = 0; i < columnNames.length; i++ ) {
			builder.append(",");
			builder.append(columnNames[i]);
		}
		builder.append("\n");
		cursor.close();

		try {
			FileOutputStream stream = new FileOutputStream(new File(okhttp_folder + "/" + filename));
			BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));

			cursor = contentResolver.query(listing, null, null, null, null);
			int columnCount = cursor.getColumnCount();
			int rowCount = 0;

			// 画像のデータを取得
			Boolean isData = null;
			while ( isData == null ? (isData = cursor.moveToFirst()) : (isData = cursor.moveToNext()) ) {

				rowCount++;
				builder.append(rowCount);
				for ( int i = 0; i < columnCount; i++ ) {
					builder.append(",");
					builder.append(cursor.getString(i));
				}
				builder.append("\n");

			}

			// 全てを書込み
			writer.write(builder.toString());

			writer.flush();
			writer.close();
			Log.i("lightbox","CSV出力完了");

		} catch (Exception e) {
			e.printStackTrace();
		}

	}



posted by lightbox at 2017-01-23 22:47 | 2017 Android Studio | このブログの読者になる | 更新情報をチェックする
Seesaa の各ページの表示について
Seesaa の 記事がたまに全く表示されない場合があります。その場合は、設定> 詳細設定> ブログ設定 で 最新の情報に更新の『実行ボタン』で記事やアーカイブが最新にビルドされます。

Seesaa のページで、アーカイブとタグページは要注意です。タグページはコンテンツが全く無い状態になりますし、アーカイブページも歯抜けページはコンテンツが存在しないのにページが表示されてしまいます。

また、カテゴリページもそういう意味では完全ではありません。『カテゴリID-番号』というフォーマットで表示されるページですが、実際存在するより大きな番号でも表示されてしまいます。

※ インデックスページのみ、実際の記事数を超えたページを指定しても最後のページが表示されるようです

対処としては、このようなヘルプ的な情報を固定でページの最後に表示するようにするといいでしょう。具体的には、メインの記事コンテンツの下に『自由形式』を追加し、アーカイブとカテゴリページでのみ表示するように設定し、コンテンツを用意するといいと思います。


※ エキスパートモードで表示しています

アーカイブとカテゴリページはこのように簡単に設定できますが、タグページは HTML 設定を直接変更して、以下の『タグページでのみ表示される内容』の記述方法で設定する必要があります

<% if:page_name eq 'archive' -%>
アーカイブページでのみ表示される内容
<% /if %>

<% if:page_name eq 'category' -%>
カテゴリページでのみ表示される内容
<% /if %>

<% if:page_name eq 'tag' -%>
タグページでのみ表示される内容
<% /if %>
この記述は、以下の場所で使用します
container 終わり



フリーフォントで簡単ロゴ作成
フリーフォントでボタン素材作成
フリーフォントで吹き出し画像作成
フリーフォントではんこ画像作成
ほぼ自由に利用できるフリーフォント
フリーフォントの書体見本とサンプル
画像を大きく見る為のウインドウを開くボタンの作成

CSS ドロップシャドウの参考デモ
イラストAC
ぱくたそ
写真素材 足成
フリーフォント一覧
utf8 文字ツール
右サイド 終わり
base 終わり