BridgeMailer for Mac
この時期なのにわりとあっぷあっぷです。だんだんと暑くなってきて溶けそうな状態になりつつあります。こんな時はプールに行って端っこの方で延々と浸かっていたいです。(泳ぎたいのではないところに注意)そんなこんなで某社のエクステンション周りを確認中です。でも、まだまだ各アプリに実装が追いついてない部分があったりで???な部分も残っています。NPMとか各種フレームワークが利用出来る様になったのは評価出来ます。しかし、バックボーンはExtendScriptであり、今回のリリースでは前回のオブジェクトモデルとほぼ変わらないといった。手抜きもあります。何よりHTMLパネルというのはFlashインターフェースと比較するとパフォーマンスを出しにくいのも変わっていません。この辺りの事については某社的には知らない振りであり、ハードウェアの進化に任せようと言った姿勢が見え隠れしてもにょ〜ってなる原因でもあります。
ただの愚痴にしかなっていませんが、詳細な解説やサンプル等は順次情報を公開予定です。もう少しお待ちください。
今回はBridgeからダイレクトにメールを送信できる様にしてみました。ファイル添付も可能ですので、一度チェックしてみて下さい。
毎度の事なのですが、某社のサンプルをベースにしようと思って色々いじろうとすると、最後には跡形も無くなってしまっています。以前socket classのテストでSMTPの事は書いてあったと思うのでプロトコルについては解説しません。ただ、以前と少し違う事もあります。例えば、SMTP before POPなんて仕組みが必要になったとかですが、その辺りも盛り込んであります。(サンプルが!)
SDKサンプルに含まれているものは全般的に良くできたサンプルです。ただ、実用には耐えません。スループットがダメダメなんです。
そこをごまかす為にバックグランドにタスクを送ったりしていますが、大容量をエンコードしようとするとやはりハングに近い状態に陥ります。そんなこんなでテンポラリ使えよって思うわけです。さらに一歩踏み込んでExternalObjectで高速化もしたのですが、しかしながら、直接エンコ処理した文字列を返すと、これまた受け側があっぷあっぷします。
ですからこちらもテンポラリを利用します。こうしておけば文字数がいくら多かろうが、一行あたり76文字のデータをreadlnで読み出して逐次処理できるので大きな負荷にはなりません。
そして、一番の問題が、ロケールだったりします。元々は英語に対応させる事しか考えられていません。asciiだけ考えれば良いのであればどんなに楽かw普通はISO2022なのでしょうが、最近のメーラーは普通にUTF-8を通してくれます。ですからUTF-8で処理を考えるのですが、これがまた一手間増やす要因だったりするのです。そこら辺をこねこねするとほとんど別物になるという…
まずは動作説明から。
Bridgeのヘルプメニューの一番下にメールアカウントの設定が設けてあります。ここで一連の情報を登録しておきます。入力されたデータはBridgeの設定データとして保持されます。この項目では簡易チェック機能も盛り込んでありますのでサーバーにアクセス出来ない、又は、アカウント情報に誤りがある場合、メッセージが表示されて再入力を促します。
メニューを選ぶとこんな感じです。
次にサムネイルを右プレスしてメニューを表示すると、一番下にメール送信メニューが追加されています。この項目を選択すると、
この様にメール送信用のダイアログが表示され、宛先・タイトル・本文が入力出来ます。ここで一つ困ったことに本文のマルチラインのedittextが改行を入力出来ません。どうしたものやら…という事です。ちょっと考え中なのでこのままリリースしておきます。テキストエディタからコピペすると改行はいるんですがねぇ…
Sendボタンをクリックで以降のプロセスがバックグランドに潜り実行されます。このタスクスケジューラー、非常に便利です。Illustratorにも付けばいいのに…
こんな感じなのですが、まずはスクリプトを置いておきます。
BridgeMailer.jsx.zip
今回のものはFrameworkを要求します。
b6464.framework.zip
こちらは起動ディスクの第一階層(通常「Macintosh HD」へ、ぽいってやればOK)に置いておいて下さい。
さて、ここから先はマニアックなお話が展開する部分です。スクリプトが良くわからなくっても結構ですのでチェックしてみて下さいね。
とりあえず各コードをおおまかに解説して行きましょう。
function BridgeMailer() {
BridgeMailer.mailServerName = app.preferences.popServer;
BridgeMailer.username = app.preferences.popUser;
BridgeMailer.password = app.preferences.passwd;
BridgeMailer.sender = app.preferences.sender;
BridgeMailer.POP = 110;
BridgeMailer.SMTP = 25;
BridgeMailer.sendTaskID = 0;
BridgeMailer.attach = false;
BridgeMailer.Thumbnails = [];
BridgeMailer.socket = new Socket();
BridgeMailer.socket.encoding = "binary";
BridgeMailer.isRunning = false;
BridgeMailer.boundary = "****=_NextPartMyBody_0000 " + new Date().getSeconds();
}
パラメータを拾い上げるだけなのですが、頭の数行に注目を。パラメータをプレファレンスから取得しています。Bridgeではユーザーがカスタム設定を作る事が許されています。これらのカスタム設定は他の設定と共にBridgeのplistに収容されます。
BridgeMailer.connect = function(host, port) {
if(!BridgeMailer.socket.open(host + ":" + port)) return false;
return true;
}
主に接続テストに利用するものですが、socketを扱うときの参考になるでしょう。
続いて認証処理を行なうルーチンです。
BridgeMailer.authorise = function() {
try {
BridgeMailer.doCommand("USER " + BridgeMailer.username, "pop")
BridgeMailer.doCommand("PASS " + BridgeMailer.password, "pop");
BridgeMailer.doCommand ("QUIT", "pop");
} catch(o_O) {
alert ("BridgeMailer.authorise() Error");
return false;
}
return true;
}
こちらは、ほぼ処理手順の記述のみです。実際のソケット処理は次のファンクションにゆだねられます。
BridgeMailer.doCommand = function(cmd, type) {
var reply = "";
BridgeMailer.socket.write(cmd + "\r\n");
if (type.match(/pop|smtp/)==null) {
BridgeMailer.close();
return false;
}
$.sleep(300);
reply = BridgeMailer.socket.read();
if(type=="pop"&&reply.substring(0,1)=="-"||type=="smtp"&&!reply) {
BridgeMailer.close();
app.cancelTask(BridgeMailer.sendTaskID);
BridgeMailer.isRunning = false;
throw "Command error for: " + type + " - " + cmd;
}
return true;
}
主として利用するコマンドはPOPとSMTPです。その他は必要ないのですが、一応コマンド投げて返り値を破棄して停止する仕様です。また、前記のコマンドについてもpopなら返り値が「-」で始まるものとか、SMTPではサーバーの応答が得られな場合というのがエラーになります。そこらへんもエラー処理を行なうようになっています。
BridgeMailer.close = function() {
try {
BridgeMailer.socket.close();
} catch(error) {
alert ("BridgeMailer.close(): Error closing socket: " + error);
}
}
こちらはソケットをクローズする為のファンクション。
そして、次がメッセージを入力する為のダイアログとその周辺です。
BridgeMailer.getMessage = function(){
var rslt = [];
var w = new Window ("dialog","Bridge Mailer");
var rePnl = w.add('panel',undefined, "Recipient");
var rec = rePnl.add("edittext",undefined,"");
rec.size = [400,20];
var tiPnl = w.add('panel',undefined, "Title");
var ti = tiPnl.add("edittext",undefined,"Sending Data from Bridge Mailer");
ti.size = [400,20];
var mePnl = w.add('panel', undefined,"Message");
var message = mePnl.add("edittext",undefined,"",{multiLine:true});
message.size = [400,60];
var btnGroup = w.add("group", undefined );
btnGroup.alignment = "center";
var bt = btnGroup.add("button", undefined, "Send");
var cl = btnGroup.add("button", undefined, "Cancel");
bt.onClick = function (){
if (rec.text.match(/.+@.+\..+/)==null){
alert("Check Mail address!");
return;
}
w.close();
rslt.push(rec.text);
}
cl.onClick = function (){
w.close();
app.cancelTask(BridgeMailer.sendTaskID);
rslt.push(false);
}
w.show();
rslt.push("=?UTF-8?B?" + BridgeMailer.base64(BridgeMailer.toUTF8(ti.text)) + "?=");
rslt.push(BridgeMailer.base64(BridgeMailer.toUTF8(message.text + "\r\n")));
return rslt;
}
各入力値を配列に収容してそれを返します。その際、メールアドレスの簡易構造解析とSubjectと本文の文字コード変換エンコード処理を行ないます。この辺りが日本語を扱う上での非常に煩わしい部分でもあります。
次のファンクションがメールヘッダを構築する部分です。冒頭でメッセージボディを取得する為に前述のファンクションを呼び出したり、POP認証を行なったりしています。各処理にはエラー条件がありますので、ひっかかった場合は全て処理の中止処理を行なう事になります。
BridgeMailer.send = function() {
if(BridgeMailer.Thumbnails.length != 0) {
BridgeMailer.isRunning = true;
var messe = BridgeMailer.getMessage();
if (!messe[0]) {
BridgeMailer.isRunning = false;
return;
}
if(!BridgeMailer.connect(BridgeMailer.mailServerName , BridgeMailer.POP)) {
BridgeMailer.isRunning = false;
app.cancelTask(BridgeMailer.sendTaskID);
return;
}
BridgeMailer.authorise();
BridgeMailer.close();
if(BridgeMailer.connect(BridgeMailer.mailServerName , BridgeMailer.SMTP)) {
var welcome = BridgeMailer.socket.read();
BridgeMailer.doCommand ("EHLO " + BridgeMailer.sender, "smtp");
BridgeMailer.doCommand ("MAIL FROM: " + BridgeMailer.sender, "smtp");
BridgeMailer.doCommand ("RCPT TO: " + messe[0], "smtp");
BridgeMailer.doCommand ("DATA", "smtp");
BridgeMailer.socket.write ('From: "BridgeMailer" <' + BridgeMailer.sender + '>\r\n');
BridgeMailer.socket.write ("To: " + messe[0] + "\r\n");
BridgeMailer.socket.write ("Subject: " + messe[1] + "\r\n");
BridgeMailer.socket.write ("Date: " + new Date().toString() + "\r\n");
BridgeMailer.socket.write("MIME-Version: 1.0\r\n");
BridgeMailer.socket.write("Content-Type: multipart/mixed;\r\n");
BridgeMailer.socket.write('\tboundary="' + BridgeMailer.boundary + '"\r\n');
BridgeMailer.socket.write("X-Mailer: Bridge Mailer 1.0\r\n");
BridgeMailer.socket.write("\r\n");
BridgeMailer.socket.write("This is a multi-part mesage in MIME format.\r\n");
BridgeMailer.socket.write("\r\n");
BridgeMailer.socket.write("--" + BridgeMailer.boundary + "\r\n");
BridgeMailer.socket.write("Content-Type: text/plain;");
BridgeMailer.socket.write(" charset=UTF-8;");
BridgeMailer.socket.write(" delsp=yes;");
BridgeMailer.socket.write(" format=flowed;\r\n");
BridgeMailer.socket.write("Content-Transfer-Encoding: base64\r\n");
BridgeMailer.socket.write("\r\n");
BridgeMailer.socket.write(messe[2] + "\r\n");
BridgeMailer.socket.write("\r\n");
BridgeMailer.socket.write("--" + BridgeMailer.boundary + "\r\n");
BridgeMailer.attach = app.scheduleTask('BridgeMailer.sendData()', 10, true);
}
}
}
こちらでは日本語に対応する為にchar-setがUTF-8に、Transfer-Encodingがbase64に設定されている事にご注意ください。
続くこのファンクションが添付処理です。ファイルデータはバイナリとして読み込みbase64で符号化された物がメールに盛り込まれます。その際、ファイル名称等も処理を行ないます。
BridgeMailer.sendData = function() {
app.document.status = "BridgeMailer.sendData is sending files...";
BridgeMailer.thumb = BridgeMailer.Thumbnails.pop();
var fp = BridgeMailer.thumb.path;
BridgeMailer.socket.write("Content-type: " + BridgeMailer.thumb.mimeType + ";\r\n");
BridgeMailer.socket.write('\tname="=?UTF-8?B?'
+ BridgeMailer.base64(BridgeMailer.toUTF8(BridgeMailer.thumb.name)) + '?="\r\n');
BridgeMailer.socket.write("Content-Disposition: attachment;\r\n");
BridgeMailer.socket.write('\tfilename="=?UTF-8?B?'
+ BridgeMailer.base64(BridgeMailer.toUTF8(BridgeMailer.thumb.name)) + '?="\r\n')
BridgeMailer.socket.write("Content-Transfer-Encoding: base64\r\n");
BridgeMailer.socket.write("\r\n");
BridgeMailer.encodeString(fp);
if(BridgeMailer.Thumbnails.length!=0) {
BridgeMailer.socket.write("--" + BridgeMailer.boundary + "\r\n");
return true;
}
else {
app.scheduleTask('BridgeMailer.finishEmail()', 10, false);
app.cancelTask(BridgeMailer.attach);
return;
}
return true;
}
こちらのファンクションはAdobe純正に近いのですが、少し改変してあります。
大きな容量の物を通すとスループットが恐ろしい事になるので、こちらは本文やタイトル、ファイル名等を処理する為のものです。
BridgeMailer.base64 = function (str){ //encoder use for short strings only
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var encoded = "";
var c1, c2, c3;
var e1, e2, e3, e4;
var i = 0;
while(i<str.length){
c1 = str.charCodeAt(i++);
c2 = str.charCodeAt(i++);
c3 =str.charCodeAt(i++);
e1 = c1 >> 2;
e2 = ((c1 & 3) << 4) | (c2 >> 4);
e3 = ((c2 & 15) << 2) | (c3 >> 6);
e4 = c3 & 63;
if (isNaN(c2)){
e3 = e4 = 64;
}
else if (isNaN(c3)) {
e4 = 64;
}
encoded = encoded + keyStr.charAt(e1) + keyStr.charAt(e2) +
keyStr.charAt(e3) + keyStr.charAt(e4);
}
return encoded;
}
そして、これはExternalObjectを利用して高速動作するエンコーダを利用したファンクションです。
BridgeMailer.encodeString = function(fpath) { //for encode target object
if(fpath==undefined) return;
var sampleLib = new ExternalObject("lib:/b6464.framework");
sampleLib.b64String(fpath);
sampleLib.unload();
var tmpf = new File("/tmpB64");
tmpf.open("r");
while(!tmpf.eof) BridgeMailer.socket.write(tmpf.readln()+"\r\n");
tmpf.close();
tmpf.remove();
}
引数で渡されたファイルパスからバイナリを読み込みテンポラリファイルに順次落とし込みます。
下のものはコードコンバーターです。UnicodeからUTF-8の文字コードへと変換します。
BridgeMailer.toUTF8 = function (str) { //Convert Unicode toUTF-8
var cd, rslt = "";
for (var i=0;i<str.length;i++) {
cd = str.charCodeAt(i);
if (cd<=0x7f) {
rslt += str.charAt(i);
}
else if (cd>=0x80 && cd<=0x7ff) {
rslt += String.fromCharCode(((cd >> 6) & 0x1f) | 0xc0);
rslt += String.fromCharCode((cd & 0x3f) | 0x80);
}
else {
rslt += String.fromCharCode((cd >> 12) | 0xe0);
rslt += String.fromCharCode(((cd >> 6) & 0x3f) | 0x80);
rslt += String.fromCharCode((cd & 0x3f) | 0x80);
}
}
return rslt;
}
そして、メール送信処理の終端処理です。
BridgeMailer.finishEmail = function() {
BridgeMailer.socket.write("\r\n");
BridgeMailer.socket.write("--" + BridgeMailer.boundary + "--\r\n");
retval = BridgeMailer.doCommand(".", "smtp");
BridgeMailer.doCommand("QUIT", "smtp");
BridgeMailer.close();
BridgeMailer.isRunning = false;
app.document.status = "BridgeMailer has finished emailing files.";
}
一番最後の行に注目ください。
ステータスバーにメッセージを表示する為のプロパティです。この様にdocument.statusに直接文字列を書き込みます。
最後のファンクションです。メニュー登録の為のルーチンを。
BridgeMailer.prototype.run = function() {
try {
var emailMenuItem = new MenuElement("command", "BridgeMailer: Send by Email",
"at the end of Thumbnail", "BridgeMailerMenu");
emailMenuItem.onSelect = function(m) {
if(!BridgeMailer.isRunning) { // Check the status of the background operation
var cachedSelections = app.document.selections;
BridgeMailer.Thumbnails = [];
for(var i=0;i<cachedSelections.length;i++) {
if(cachedSelections[i].container) continue;
BridgeMailer.Thumbnails.push(cachedSelections[i]);
}
if(BridgeMailer.Thumbnails.length>0) {
BridgeMailer.sendTaskID = app.scheduleTask('BridgeMailer.send()', 10, false);
}
}
}
emailMenuItem.onDisplay = function(m) {
var selLength = app.document.selectionLength;
if(selLength>0) this.enabled = true;
else this.enabled = false;
}
var emailPref = new MenuElement("command", "Email setting...",
"at the end of Help", "BridgeMailerSetting");
emailPref.onSelect = function() {
var props =["popServer","popUser","sender","passwd","smtp","smtpPort"];
var val = [];
for (var i=0;i<props.length;i++)
eval("val[" + i + "]=app.preferences." + props[i]);
var w = new Window ("dialog","Email Settings");
var pnl0 = w.add('panel',undefined, "mail server");
var pop = pnl0.add('edittext', undefined, val[0]);
pop.characters = 40;
var pnl1 = w.add('panel',undefined, "user name");
var usr = pnl1.add('edittext', undefined, val[1]);
usr.characters = 40;
var pnl2 = w.add('panel',undefined, "mail address");
var sdr = pnl2.add('edittext', undefined, val[2]);
sdr.characters = 40;
var pnl3 = w.add('panel',undefined, "password");
var pwd = pnl3.add('edittext', undefined, val[3],{noecho:true});
pwd.characters = 40;
var pnl4 = w.add('panel',undefined, "smtp server");
var smt = pnl4.add('edittext', undefined, val[4]);
smt.characters = 40;
var pnl5 = w.add('panel',undefined, "smtp port");
var spt = pnl5.add('edittext', undefined, val[5]);
spt.characters = 40;
var btnGroup = w.add("group", undefined );
btnGroup.alignment = "center";
var bt = btnGroup.add("button", undefined, "OK");
var cl = btnGroup.add("button", undefined, "Cancel");
bt.onClick = function (){
if (!BridgeMailer.connect(pop.text, BridgeMailer.POP)){
alert("Could not connect POP server, check this...");
return;
}
app.preferences.popServer = pop.text;
app.preferences.popUser = usr.text;
app.preferences.sender = sdr.text;
app.preferences.passwd = pwd.text;
app.preferences.smtp = smt.text;
app.preferences.smtpPort = spt.text;
BridgeMailer.mailServerName = app.preferences.popServer;
BridgeMailer.username = app.preferences.popUser;
BridgeMailer.password = app.preferences.passwd;
BridgeMailer.sender = app.preferences.sender;
if (!BridgeMailer.authorise()) return;
w.close();
}
cl.onClick = function (){
w.close();
}
w.show();
}
} catch(error) {
alert (error);
return false;
}
return true;
}
無駄に長くなっているのは追加するメニュー項目が2つだからです。また、設定処理をダイレクトにonSelectに含めたのも一因です。メニューへの登録については過去記事をご覧下さい。
new BridgeMailer().run();
最後にスタートアップ時に実行される一行です。
細かい所をみると、結構Bridge独自の実装に対する思想が見え隠れして面白い物です。間違いなくAdobe社のアプリの中では一歩先に進んでいます。同じExtendScriptと言えどここまで差異が出ると言うのも問題です。