戻る(メールソフト方面)

戻る(FAX方面)




BSMTPという便利なdllファイルがありまして。
これを使うと誰でもメール送受信ソフトが作れます。
プログラミング言語はたぶん何でもいいんでしょうけど、VBやVBAを使ったBSMTPメールソフトについてはそこらじゅうに解説ページがあるうえ、作者本人によるお助けツールみたいなものまで揃っているらしいので、とりあえずさておくとして、ここではDelphiを使った方法ってのを考えてみましょう。

Delphi + BSMTP という組み合わせは、web上でもなぜか情報が少ない。
単にDelphiを使っている人が少ないというだけのことなのかもしれません。
(知らなかったんですが、自分が使い始めた2004〜5年ごろにはすでに凋落の傾向が顕著だったという噂。これを悲しがるほどDelphiに習熟しているわけじゃないけど、自分程度の人間でもどうにか動くものがつくれる言語は他にほとんどないという理由で、絶滅されるとやっぱり困ってしまう)

どこかのページで、どこかの誰かがおそらくは作者本人に向けて、「BSMTPはDelphiでも使えますね」というような報告をしていました。
もしかしたらBSMTP.dllの作者本人も、Delphiのことなんて頭になかったのかもしれません。
それを読んで、何となく、Delphiでつくってみたくなった。

とりあえずBSMTP.dllをダウンロードして(検索であっという間に見つかる)、exeファイル(というかこの場合はソースファイル)のある場所に置いてください。
これが同フォルダ中にないと何も始まらない。

dllファイルをものにしてしまえば、あとはWindowsAPI関数と同じような要領で使えそうです。
以下を適当な場所に書いてください。
type
  {unit1に押し込むなら TForm1 = class(TForm) のすぐ上あたり。
  隔離unitを別に誂えるなら、interface uses のすぐ下あたり、かな}
  TBSMTPStatus = record
    Counter: integer;
    Status : array[0..255] of LPSTR;
  end;
  TPBSMTPStatus = ^TBSMTPStatus;



{↓API関数はどこに書くのが正しいのか、実は知らない(そんなことよりこれはAPIじゃないだろ)}
function BSendMail(szServer, szTo, szFrom, szSubject, szBody, szFile: LPCSTR;
                    msg: LPSTR): integer; cdecl external 'Bsmtp.dll';
function BMIME(szCommand, szFileName, szPath: LPCSTR;
                Status: TPBSMTPStatus; msg:
                LPSTR):integer; cdecl external 'Bsmtp.dll';
function BPOP3(szServer, szName, szPass, szCommand, szPath: LPCSTR;
                Status: TPBSMTPStatus;
                msg: LPSTR): integer; cdecl external 'Bsmtp.dll';
procedure BFreeArray(Status: TPBSMTPStatus); cdecl external 'Bsmtp.dll';
{隔離unitの場合は implementation のすぐ下、Form付随unitの場合は $R *.dfm の下あたりでいいんじゃなかろうか。
以前はもっと上のほう、他の自作関数の宣言と混ぜてAPI関数を書いてたけど、それでもちゃんと動いてたような気がする。
さらに思い出してみると、入れ子関数の子供として書いたこともあるような気がする。
要は使う場所より上に書いてあれば、というか、使う場所から参照可能なところなら、どこでもいいんじゃね}


細かいことはどうでもいいとして。
何ができればメールソフトとして用が足りるのか。
大雑把にいっちゃえば、受ける、送る、読む、の3つが出来ればそれで合格。
単独処理として考えるなら、コード書きの面倒くささは、「読む」>「送る」>「受ける」の順番。


まずは「受ける」
ただ受けるだけならまったく単純。
BSMTP関数としてすでに宣言されているBPOP3関数を、素っ裸のまま使っちゃってもいい。
var
  MySTBPOP3: TPBSMTPStatus;
  MyWKSTR: array[0..80] of Char;
  MyRtn: integer;
begin
  New(MySTBPOP3); {←これ忘れないで}

  MyRtn:=BPOP3(PChar('POP3文字列'),
              PChar('ユーザーネーム文字列'),
              PChar('パスワード文字列'),
              PChar('SAVEALLD'),
              PChar('受信したメールの保存先(ディレクトリ)文字列'),
              MySTBPOP3, MyWKSTR);

  BFreeArray(MySTBPOP3); {←これも忘れないで}
end;
それぞれの文字列は、余計な加工不要。
そのまんま突っ込んじゃってください。

保存先ディレクトリはどこにどんな名前で用意しても自由ですけど、メールファイルは、受信して保存されるときに自動的に名前がつけられます(20110514-14362859000.txt みたいな形)
後で書き換えるのはそれぞれの勝手として、受信した時点での名前指定はたぶん無理。

このBPOP3関数は、受信すると1以上の値を返します。
メールを一通受けたら1、二通受けたら2を返すのか、多分違うんじゃないかと思いますが、そんなことはどうでもいいとして、ゼロなら「受信しなかった」または「受信に失敗した」ということ。


次は「送る」
これもBSMTP関数のひとつ、BSendMailで素っ裸のままいっちゃいますね。
var
  MyWKSTR: array[0..80] of Char;
  MyRtn: integer;
begin
  MyRtn:=BSendMail(PChar('SMTP文字列'),
                  PChar('送り先アドレス文字列'),
                  PChar('送り主アドレス文字列'),
                  PChar('タイトル文字列'),
                  PChar('本文文字列'),
                  PChar('添付ファイルのフルパス文字列'),
                  MyWKSTR);
end;
前後にNewだのFreeだの書く必要がない分、受信よりも楽そうですけど、実はそうでもありません。
ここに突っ込む文字列の多くは、そのまんまじゃダメ。
いちいち面倒な加工が必要。

SMTP文字列は、マシン名 mail.hoge.ne.jp の後に ':ポート番号' を付けなきゃなりません。
PChar('mail.hoge.ne.jp:587') とか PChar('mail.hoge.ne.jp:25') とか。
好みしだいでさらにその後、タイムアウト指定なんかも。
PChar('mail.hoge.ne.jp:25:60') みたいな感じで。

送り先については、「相手がたった一人で、なおかつ送信したものをファイルとして保存する気がない」なら、ただアドレスを入れるだけでいいような気がするんですけど、たいていの場合、送信したメールってのは保存しとくもんですよね。

'相手先アドレス' + tabで区切って + '>#save ' + '保存先ディレクトリ'
保存先ディレクトリはもちろん好きに決められます(ファイル名は、受信の場合と同様、自動)
受信メールの保存場所とは別のところにしたほうがいいような気がします(←あたりまえだバカ)
PChar('punipuni@yoyo.ne.jp' + Chr(9) + '>#save ' + ExtractFilePath(Application.ExeName) + 'sentmail0') みたいな感じ、か。

相手が複数の場合の区切りもtab。
PChar('address1' + Chr(9) + 'address2' + Chr(9) + 'address3' + Chr(9) + '>#save ' + '保存先')

相手が複数で cc, bcc指定したい場合は、頭にくっつけたうえやっぱりtabで区切る。
PChar('cc' + Chr(9) + 'address1' + Chr(9) + 'address2' + Chr(9) + 'address3' + Chr(9) + '>#save ' + '保存先') とか
PChar('bcc' + Chr(9) + 'address1' + Chr(9) + 'address2' + Chr(9) + 'address3' + Chr(9) + '>#save ' + '保存先') とか

送り主のメールアドレスは、寛容な接続サービスならストレートにそれだけでもOK。
PChar('roro@hoge.ne.jp')

不寛容な、もとい、SMTP認証を通さなきゃならないサービス会社の場合は、'アドレス' + tabで区切って + 'ユーザー名:パスワード'
さらにtabで区切って認証法。
PChar('roro@hoge.ne.jp'+ Chr(9) + 'roro:myroro123' + Chr(9) + 'LOGIN')とか
PChar('roro@hoge.ne.jp'+ Chr(9) + 'roro:myroro123' + Chr(9) + 'CRAM-MD5')とか
PChar('roro@hoge.ne.jp'+ Chr(9) + 'roro:myroro123' + Chr(9) + 'PLAIN')とか

'LOGIN'の場合は、 + Chr(9) + 'LOGIN' を省略して PChar('roro@hoge.ne.jp'+ Chr(9) + 'roro:myroro123') でもOK。
認証法が省略されたものは、自動的に'LOGIN'と解釈されるようです。

タイトル文字列、本文文字列は加工不要として、最後は添付ファイル文字列。
ファイルが1個ならフルパスそのまんま。
PChar('C:\AUTOEXEC.BAT')

複数ある場合はやっぱりtab区切り
PChar('C:\AUTOEXEC.BAT'+ Chr(9) + 'C:\CONFIG.SYS' + Chr(9) + 'C:\setup.log')

このBSendMail関数も、BPOP3関数同様、リターンが1以上なら送信成功、ゼロなら失敗です。


受信、送信の二つは比較的単純なので、BSMTP関数素っ裸使用でもどうにか出来ますが、メールファイルを読む、というか「読めるようにする」のは、一筋縄ではいきません。
自分でいくつか関数を作らないと。

function GetCntFiles(strDir: string): integer; {フォルダ内のファイル数をカウントする関数}
var
  MySrc: TSearchRec;
  MyCnt: integer;
begin
  MyCnt:=0;
  if FindFirst(strDir + '\*.*', faAnyFile, MySrc)=0 then begin
    try
      repeat
        if (MySrc.Name<>'.') and (MySrc.Name<>'..') then begin
          MyCnt:=MyCnt + 1;
        end;
      until FindNext(MySrc)<>0;
    finally
      FindClose(MySrc);
    end;
  end;
  Result:=MyCnt;
end;

function GetStrMailAddress(strF: string): string; 
{ある文字列 strF から、メールアドレスと思われる文字列を取り出したうえ、
それが複数あった場合は適当な記号で区切ったカタチにして返す関数(usesにComObjを加える必要あり)}
const
  cMLDLMTR='   '; {区切り記号。完全にテキトーでいい。この記号は、後々場面に応じて別の記号に置換されるという想定なので}
  cPATMLSVR='([\w_\-]+)\.([\w_\.\-]*)[a-z][a-z]';
  cPATMLADR='([\w]+)([\w\.-]+)@' + cPATMLSVR;
var
  MyStrF, MyStrR: string;
  MyRegExp, MyMatch: OleVariant;
  MyCnt, MyInc: integer;
begin
  MyStrF:=AnsiReplaceStr(strF, '"', '');
  MyRegExp:=CreateOleObject ('VBScript.RegExp');
  MyRegExp.Pattern:=cPATMLADR;
  MyRegExp.Global:=true;
  MyRegExp.IgnoreCase:=true;
  MyMatch:=MyRegExp.Execute(MyStrF);
  MyCnt:=MyMatch.Count;
  MyStrR:='';
  for MyInc:=0 to MyCnt-1 do begin
    MyStrR:=MyStrR + MyMatch.Item[MyInc].Value + cMLDLMTR;
  end;
  MyMatch:=Unassigned;
  MyRegExp:=Unassigned;
  MyStrR:=TrimRight(MyStrR);
  Result:=MyStrR;
end;
上の2つは言ってみれば補助関数。
メールを読むのに使うのは、BSMTP関数のうちのBMIME です。

BMIME('HEAD', PChar(ファイルフルパス), PChar(展開先ディレクトリ),〜) で、ヘッダ取得
BMIME('GETNOF', PChar(ファイルフルパス), PChar(展開先ディレクトリ)〜) で、ヘッダと本文取得
BMIME('GET', PChar(ファイルフルパス), PChar(展開先ディレクトリ)〜) で、添付ファイルまでひっくるめた全てを取得

ただ、これでMyRtn:=BIME('HEAD'〜)みたいなことをやっても、0とか1の値が返って来るだけです。

「このメールのヘッダ情報をくれ」「取得成功しました」
「ヘッダと本文、全部くれ」「取得成功しました」
「だから、くれって言ってるの」「取得成功しました」
「お前なあ・・・」「取得失敗しました」
こんなアホなやり取りを繰り返していては、いつまでたってもメールは読めません。

取得したその中身を見るには
  if ヘッダ取得に成功したら then begin
    for i:=0 to ヘッダ中の区画みたいなものの数-1 do begin
      〜区画ごとに、本当に欲しいものの取得処理〜
    end;
  end;
というような書き方をする必要があります。

気分的にはヘッダを一行ずつ処理しているのとさほど違いはないんですけど、どうもそういうわけではなさそうで。

実際に覗いてみるとわかりますけど、ヘッダにしろ本文にしろ、メールファイルのソースには、「何行目」なんてものはたぶん存在しません。
どこが情報の区切れ目になっているのか分からないぐらいグチャグチャです。

メールファイルには、「改行記号」によって区切られた「行」とは違う、何か分からないものによって区切られた不思議な単位が存在するようです。
(「行」とは違う、断じて違う、それははっきりしているんですが、この先話を進める都合上、一種のたとえとしてこの言葉を使うことがあるかもしれません。あしからず)
procedure GetMLHData(strFile: string; var strDt, strTo, strFrom, strSbj, strCc: string);
const
  cHDATA: array[1..5] of string=('date: ', 'to: ', 'from: ', 'subject: ', 'cc: ');
var
  MyStrLine: string; {ここですでに「行」っぽい意味というか雰囲気を漂わせた変数名にしちゃってる}
  MyStrHData: array[1..5] of string;
  MyIncL, MyIncH: integer;
  MySTBMIME: TPBSMTPStatus;
  MyChrArr: array[0..80] of Char;
begin
  for MyIncH:=1 to 5 do begin
    MyStrHData[MyIncH]:=''; {どうでもいいけど初期値は全部空文字}
  end;

  New(MySTBMIME);

  if BMIME(PChar('HEAD'), PChar(strFile), PChar('C:\'), MySTBMIME, MyChrArr)>=1 then begin
    {ヘッダ取得。
    とりあえず「日付時刻、宛先、差出人、タイトル、cc文字列」を取りたいだけ。
    本文と添付ファイルについては、ここでは相手にするつもりはないので、
    展開先ディレクトリは、デタラメ'C:\'で決め打ち。
    実際に展開は行われないんだから、デタラメどころか空文字でもいいんじゃないか
    と思ったけど、ちゃんと存在するディレクトリでなきゃダメらしい}

    for MyIncL:=0 to MySTBMIME.Counter-1 do begin
      MyStrLine:=MySTBMIME.Status[MyIncL]; {1行ずつとっては、その文字列を精査}
      for MyIncH:=1 to 5 do begin
        if LowerCase(LeftStr(MyStrLine, Length(cHDATA[MyIncH])))=cHDATA[MyIncH] then begin
          {頭の6文字が'date: 'ならばそこは日付時刻の行、
          頭の4文字が'to: 'ならそこは宛先の行、という調子で}

          if (MyIncH=2) or (MyIncH=3) or (MyIncH=5) then begin
            MyStrHData[MyIncH]:=GetStrMailAddress(MyStrLine);
            {宛名、差出人、cc の行では、アドレス抜き出し関数を使用。
            ぐちゃぐちゃの状態からアドレスだけ取り出し、 後々使いやすいように
            区切り記号を使ってキレイに並べた形にしておく}
          end else begin
            MyStrHData[MyIncH]:=Trim(AnsiReplaceText(MyStrLine, cHDATA[MyIncH], ''));
            {日付時刻、タイトルの行は、頭の数文字を削るだけで、
            求めているデータの形になる(はず。たぶん)}
          end;
        end;
      end;
    end;
  end;

  BFreeArray(MySTBMIME);

  strDt:=MyStrHData[1];
  strTo:=MyStrHData[2];
  strFrom:=MyStrHData[3];
  strSbj:=MyStrHData[4];
  strCc:=MyStrHData[5];
end;
この関数の使用例は↓(メールの一覧表示)
var
  MySrc: TSearchRec;
  MyStrM: array[0..5] of string; {0:ファイル名; 1:日付時刻; 2:宛先; 3:差出人; 4:タイトル; 5:Cc}
begin
  if FindFirst(strDir + '\*.txt', faAnyFile, MySrc)=0 then begin
    {保存されているフォルダの中を探し回って、全てのメールファイルをGetMLHData関数に通す}
    try
      repeat
        MyStrM[0]:=strDir + '\' + MySrc.Name;
        GetMLHData(MyStrM[0], MyStrM[1], MyStrM[2], MyStrM[3], MyStrM[4], MyStrM[5]);
        {な感じで取得したデータ、〜 MyStrM[5]を、適当なグリッドにぺたぺた貼り付けてみたり}

      until FindNext(MySrc)<>0;
    finally
      FindClose(MySrc);
    end;
  end;
end;

一覧表示が済んだら最後はメール本文と添付ファイル取得。

前述したように、BMIME('HEAD'〜)でヘッダ取得、BMIME('GETNOF'〜)でプラス本文取得、BMIME('GET'〜) で何もかも取得。
で、本文「だけ」を取得する便法ってのは用意されてないんですね。
おかげさまで↓みたいなヨレヨレの関数になっちまいました。
function TForm1.GetStrMLBody(strFile: string): string;
var
  MyFSO: OleVariant;
  MySTBMIME: TPBSMTPStatus;
  MyChrArr: array[0..80] of Char;
  MyStrTmpDir, MyStrALL, MyStrHdr, MyStrBody: string;
  MyCntAFiles, MyIntMax, MyInc: integer;
begin
  Result:='';

  MyStrTmpDir:=pbVarAppExePath + 'tmpML';
  {添付ファイルの展開場所(そのたびごとに削除生成)
  添付とtempをかけたわけじゃないぞ。たまたまだぞ}
  MyFSO:=CreateOleObject('Scripting.FileSystemObject');
  if MyFSO.FolderExists(MyStrTmpDir)=true then begin
    MyFSO.DeleteFolder(MyStrTmpDir);
  end;
  MyFSO.CreateFolder(MyStrTmpDir);
  MyFSO:=Unassigned;

  New(MySTBMIME); {添付ファイルを指定した場所に展開}
  BMIME('GET', PChar(strFile), PChar(MyStrTmpDir), MySTBMIME, MyChrArr);
  BFreeArray(MySTBMIME);

  MyCntAFiles:=GetCntFiles(MyStrTmpDir); {展開して得られた添付ファイルの数}

  MyStrHdr:='';
  New(MySTBMIME);
  {ヘッダの全文字取得
  ここでは、日付だのアドレスなんてものはどうでもいい
  欲しいのはヘッダの文字数}
  if BMIME(PChar('HEAD'), PChar(strFile), PChar(MyStrTmpDir), MySTBMIME, MyChrArr)>=1 then begin
    MyIntMax:=MySTBMIME.Counter - MyCntAFiles - 2;
    for MyInc:=0 to MyIntMax do begin
      {このループカウントMAX値はまったく不可解。
      本当は-2じゃなく-1の気がするんだけど、
      それだとヘッダがいっぱい取れすぎ。
      次の処理で全文からこのヘッダの文字数を引くと、本文のアタマが少し切れる。
      だからといって-2にすると、次の処理で本文のアタマに少し余計な文字が付く}

      MyStrHdr:=MyStrHdr + MySTBMIME.Status[MyInc];
    end;
  end;
  BFreeArray(MySTBMIME);
  if MyStrHdr='' then begin
    exit;
  end;

  MyStrALL:='';
  New(MySTBMIME); {ヘッダ+本文の全文字取得}
  if BMIME('GETNOF', PChar(strFile), PChar(MyStrTmpDir), MySTBMIME, MyChrArr)>=1 then begin
    MyIntMax:=MySTBMIME.Counter - MyCntAFiles - 1;
    for MyInc:=0 to MyIntMax do begin
      MyStrALL:=MyStrALL + MySTBMIME.Status[MyInc];
    end;
  end;
  BFreeArray(MySTBMIME);
  if MyStrALL='' then begin
    exit;
  end;

  MyStrBody:=RightBStr(MyStrALL, Length(MyStrALL) - Length(MyStrHdr));
  {取得した全文からヘッダの文字数分けずる
  (でもそれだけだと前述した通り本文のアタマに余計な文字が付く)}

  MyStrBody:=TrimLeft(MyStrBody);
  if LowerCase(LeftBStr(MyStrBody, 8))='x-uidl: ' then begin
    MyStrBody:=RightBStr(MyStrBody, Length(MyStrBody) - 41);
  end;
  MyStrBody:=TrimLeft(MyStrBody);
  if LowerCase(LeftBStr(MyStrBody, 6))='body: ' then begin
    MyStrBody:=RightBStr(MyStrBody, Length(MyStrBody) - 6);
  end;
  MyStrBody:=TrimLeft(MyStrBody);
  {仕方ないから、ちまちま削ってもっともらしく形を整える}
  Result:=MyStrBody;
end;

ところで。
メール受信ってのはそれだけの単独処理なら簡単ですけど、実際には受信中にいろんな処理をしたくなるもので。
どんな処理をしたくなるかというと、たとえば一通ずつタイトルや本文を確認して、ある条件を満たしたメールだったら自動返信するとか。

要するに受信しながら一通ずつ何かをしたい場合はどうするか、という話。

TPBSMTPStatus がここで威力を発揮します。
メールを読むときは、「一行ずつ」処理をするのに使ったけど、ここでは「一通ずつ」処理をするのに使う。
var
  MySTBPOP3: TPBSMTPStatus;
  MyWKSTR: array[0..80] of Char;
  MyInc: integer;
  MyStrFile: string;
begin
  New(MySTBPOP3);

  if BPOP3(PChar('POP3文字列'),
              PChar('ユーザーネーム文字列'),
              PChar('パスワード文字列'),
              PChar('SAVEALLD'),
              PChar('受信したメールの保存先(ディレクトリ)文字列'),
              MySTBPOP3, MyWKSTR)>=1 then begin

    for MyInc:=0 to MySTBPOP3.Counter-1 do begin
      MyStrFile:=MySTBPOP3.Status[MyInc]; {←これでファイル名が取れる}

      {ファイル名さえ取れればあとは何でもできる}
    end;
  end;

  BFreeArray(MySTBPOP3);
end;

自作メールソフト(→)でも自動返信機能はつけてますが、実をいえば上の書き方はしていません。
(手順としては、一時保存フォルダを用意したうえ、まずは受信だけを一気に済ませるようにしてる。ヘッダや本文取得は、その一時保存フォルダ中で、FindFirst。条件を満たしたものを探して、送信用のstrTo宛先文字列をつくりながら、逐次ファイルを一時保存フォルダからレギュラーの保存フォルダに移動。それらが済んだところでようやく送信Execute)
これは単純に気分の問題です。
どっちのやり方でも大して違いはありません。

ソースもアップしときます(→)けどたぶん読む気になれないと思います、
コード以前に、ファイル開いてフォームデザイン見た瞬間、嘔吐中枢のトリガーが引かれる可能性があります。
(コードがこんなに汚くなってしまった原因の多くは、常軌を逸したそのフォームデザインにありそう。いま思えば3000行超のunit1でメールのために書いたコードなんて10分の1もないんじゃなかろうかと。何というか、要するに、無計画の産物)