[PHP]PEARを使わない添付ファイル付きメール送信クラスを公開


PEARの様に依存せずに添付ファイルメールを送る方法を
私が使用するクラスをまるっと公開すると共に解説します。

内部的には、PHPの標準関数鵜「mail()」を使用し、
追加ヘッダにて添付ファイルを認識させる処理を追加しています。
今回公開するクラスは、ファイルを複数添付することができ、添付ファイル名も指定することが出来ます。
また、送信形式はCCもBCCも対応しています。

もちろん件名もファイル名も日本語OKです。
添付ファイル無しの通常メールも送信出来ます。

そのままクラスごとコピペしてもらえれば使えるかと思いますので、
じゃんじゃん使って頂いて、「もう少しこうしたら良いよ!」等のご意見が有れば
フィードバック頂ければ幸いです。

  1. 添付ファイル付きメールを送れるクラスの使い方
  2. 添付ファイルの仕組み
  3. 添付ファイル付きメールを送れるクラスのソース






添付ファイル付きメールを送れるクラスの使い方




コンストラクタでは通常の「mail()」と同じ様に、
引数として
第一引数に宛先
第二引数に件名
第三引数に本文
第四引数に追加ヘッダ
第五引数にパラメータ
を受け付けることが出来ます。

いずれも必須では無く、後にセッターメソッドにて追加することも出来ます。

生成メソッドも用意していますので、メソッドチェインにて設定したい方は
下記の様に使用出来ます。


if( ! Mail::create()
 ->name( "送信者名" )
 ->from( "送信元アドレス" )
 ->to( "送信先アドレス" )
 ->title( "件名" )
 ->body( "本文" )
 ->header( "追加ヘッダ" )
 ->param( "追加パラメータ" )
 ->cc( array("アドレス1","アドレス2") )
 ->bcc( array("アドレス1","アドレス2") )
 ->files( array( "添付ファイル表示名"=>"添付ファイルパス" ) )
 ->send()
){
 // メール送信失敗
}


基本的に、いずれのメソッドも自分のインスタンスを返却しますので、
メソッドチェインにて次々にプロパティを設定することが可能です。
また、引数を指定しなければ自動的にゲッターとなりますので、
現在の設定値を取得することも可能です。

また、メールアドレスを設定する「from()」「to()」「cc()」「bcc()」では、
デフォルトでメールアドレスの形式チェックを下記の正規表現にて行っています


preg_match( "/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/", $addr );


第二引数に「false」を指定することによって、チェック処理を除外することが出来ますので、
宛先に名前を付けたい場合などに使用してください。

同じ様に、添付ファイルを設定する「files()」でも、デフォルトでファイルの存在確認を行っています。
チェック処理を除外したい時は、第二引数に「false」を指定して下さい。

セッターメソッドによってプロパティをセットしましたら、
最終的に「send()」メソッドにてメールを送信します。
内部的には、設定されたプロパティを組み立て、「mail()」関数にてメールを送信し、
戻り値を、そのまま「send()」メソッドの戻り値としていますので、
メール送信失敗の判定は通常の「mail()」関数と同じ様に行います。





添付ファイルの仕組み




「mail()」関数にて、メールに添付ファイルを付けるには、「boundary」という考え方が必要です。

通常のメールでは、メール本文の「Content-Type」を「text/plain」として送信しますが、
添付ファイルをメールに付けるにはマルチパートで送信する必要があるので「multipart/mixed」とします。
その際に、「boundary」を指定することになりますが、この「boundary」という属性は、
異なる「Content-Type」同士の境界を示す「仕切り」の様なものです。
設定する値は任意の値で問題ありません。
「--boundary文字列」から「--boundary文字列--」までの間を一つのコンテンツとみなし、
ヘッダーを設定することが出来ます。

ですので、本文の「Content-Type」は「text/plain」
添付ファイル部分の「Content-Type」は「application/octet-stream」
という様に、内容によって正しい「Content-Type」を設定する必要が有ります。

この考え方は、HTMLメールにも言える事です。その場合、「Content-Type」は「text/html」になるでしょう。

また、日本語を可能性のある箇所は、"ISO-2022-JP"としないと文字化けしてしまうので、
MIMEに対応したエンコードを行う必要が有ります。


mb_encode_mimeheader( $file_name, "ISO-2022-JP", "B" );


注意点として、結構あちこちのブログでの解説では
ヘッダ要素にエンコードしたものをセットする際に、クオーテーションを省略していますが、
これはメーラーによっては正しく認識されないので、必ずクオーテーションで括って下さい。
MIMEエンコードしたとしても、属性に対する値は、MIMEエンコードされた「文字列」ですので、
クオーテーションで括らないとパーサーによっては上手く動作しません。
特に、スマートフォンで文字化けしてしまう可能性が有ります。


$header .= "name=\"" . mb_encode_mimeheader( $file_name, "ISO-2022-JP", "B" ) . "\"\n";
$header .= "filename=\"" . mb_encode_mimeheader( $file_name, "ISO-2022-JP", "B" ) . "\"\n";


ファイルの添付自体は、「file_get_contents()」にて添付ファイルを取得し、
「base64」にエンコードします。
そして、「chunk_split()」によって「RFC 2045」に順守した形式に変換し、含めます。


chunk_split( base64_encode( file_get_contents( $file_path ) ) )


纏めますと、ファイル添付部分は下記のようになります。


$info      = pathinfo( $file_path );
$content    = "application/octet-stream";
$filename    = mb_encode_mimeheader( $file_name, "ISO-2022-JP", "B" );
$in_file_body .= "\n";
$in_file_body .= "--" . $this->boundary() . "\n";
$in_file_body .= "Content-Type: " . $content . "; charset=\"iso-2022-jp\" name=\"" . $filename . "\"\n";
$in_file_body .= "Content-Transfer-Encoding: base64\n";
$in_file_body .= "Content-Disposition: attachment; filename=\"" . $filename . "\"\n";
$in_file_body .= "\n";
$in_file_body .= chunk_split( base64_encode( file_get_contents( $file_path ) ) ) . "\n";


また、上記はメールの本文にて設定するヘッダでしたが、
メール自体のヘッダには「Content-Type」が「multipart/mixed」であること、
そして「boundary」の識別子として使用した文字列を設定する必要が有ります。


$header .= "Content-Type: multipart/mixed; boundary=\"" . $this->boundary() . "\"\n";





添付ファイル付きメールを送れるクラスのソース




これらの解説を踏まえ、早速クラスのソースを見てみましょう。
解説した箇所が、どの様に設定されているか?処理の流れを把握して下さい。
下記のクラスは、コピペでそのまま導入しても動くかと思いますので、
お気軽にお持ちください。
もし、何か不具合が有りましたらフィードバック頂ければ幸いです。
何卒、よろしくお願いいたします。


class Mail
{
 const ENCODING   = "UTF-8";

 private $name    = "";
 private $from    = "";
 private $to     = "";
 private $title   = "";
 private $body    = "";
 private $cc     = array();
 private $bcc    = array();
 private $header   = "";
 private $param   = "";
 private $files   = array();
 private $boundary  = "";

 /* コンストラクタ
 --------------------------------------------------------------------------*/
 public function construct( $to="", $subject="", $message="", $additional_headers="", $additional_parameters="" )
 {
  $this->to    = $to;
  $this->title  = $subject;
  $this->body   = $message;
  $this->header  = $additional_headers;
  $this->param  = $additional_parameters;
 }

 /* 生成
 --------------------------------------------------------------------------*/
 public static function create( $to="", $subject="", $message="", $additional_headers="", $additional_parameters="" )
 {
  return new self( $to, $subject, $message, $additional_headers, $additional_parameters );
 }

 /* メールアドレスの形式チェック
 --------------------------------------------------------------------------*/
 public static function mailAddressValidation( $addr )
 {
  if( strlen( $addr ) <=0 ) return false;
  $result = preg_match( "/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/", $addr );
  if( $result === false || $result === 0 ) return false;
  return true;
 }

 /* 送信者名
 --------------------------------------------------------------------------*/
 public function name( $name=null )
 {
  if( is_null( $name ) ){
   return $this->name;
  }
  else{
   $this->name = $name;
   return $this;
  }
 }

 /* 送信元アドレス
 --------------------------------------------------------------------------*/
 public function from( $addr=null, $is_valid=true )
 {
  if( is_null( $addr ) ){
   return $this->from;
  }
  else{

   if( $is_valid === true ){
    if( self::mailAddressValidation( $addr ) === false ){
     throw new Exception( 'Format of the e-mail address is invalid' );
     return false;
    }
   }

   $this->from = $addr;
   return $this;
  }
 }

 /* 送信先アドレス
 --------------------------------------------------------------------------*/
 public function to( $addr=null, $is_valid=true )
 {
  if( is_null( $addr ) ){
   return $this->to;
  }
  else{

   if( $is_valid === true ){
    if( self::mailAddressValidation( $addr ) === false ){
     throw new Exception( 'Format of the e-mail address is invalid' );
     return false;
    }
   }

   $this->to = $addr;
   return $this;
  }
 }

 /* 件名
 --------------------------------------------------------------------------*/
 public function title( $title=null )
 {
  if( is_null( $title ) ){
   return $this->title;
  }
  else{
   $this->title = $title;
   return $this;
  }
 }

 /* 本文
 --------------------------------------------------------------------------*/
 public function body( $body=null )
 {
  if( is_null( $body ) ){
   return $this->body;
  }
  else{
   $this->body = $body;
   return $this;
  }
 }

 /* ヘッダ
 --------------------------------------------------------------------------*/
 public function header( $header=null )
 {
  if( is_null( $header ) ){
   return $this->header;
  }
  else{
   $this->header = $header;
   return $this;
  }
 }

 /* パラメータ(オプション)
 --------------------------------------------------------------------------*/
 public function param( $param=null )
 {
  if( is_null( $param ) ){
   return $this->param;
  }
  else{
   $this->param = $param;
   return $this;
  }
 }

 /* 添付ファイル array("ファイル名"=>"ファイルパス")
 --------------------------------------------------------------------------*/
 public function files( $files=null, $is_valid=true )
 {
  if( is_null( $files ) ){
   return $this->files;
  }
  else{

   if( $is_valid === true ){
    foreach( $files as $key => $path ){
     if( ! file_exists( $path ) ){
      throw new Exception( 'The specified file does not exist' );
      return false;
     }
    }
   }

   $this->files = $files;
   return $this;
  }
 }

 /* CC array(addr,addr)
 --------------------------------------------------------------------------*/
 public function cc( $cc=null, $is_valid=true )
 {
  if( is_null( $cc ) ){
   return $this->cc;
  }
  else{

   if( $is_valid === true ){
    foreach( $cc as $index => $addr ){
     if( self::mailAddressValidation( $addr ) === false ){
      throw new Exception( 'Format of the e-mail address is invalid' );
      return false;
     }
    }
   }

   $this->cc = $cc;
   return $this;
  }
 }

 /* BCC array(addr,addr)
 --------------------------------------------------------------------------*/
 public function bcc( $bcc=null, $is_valid=true )
 {
  if( is_null( $bcc ) ){
   return $this->bcc;
  }
  else{

   if( $is_valid === true ){
    foreach( $bcc as $index => $addr ){
     if( self::mailAddressValidation( $addr ) === false ){
      throw new Exception( 'Format of the e-mail address is invalid' );
      return false;
     }
    }
   }

   $this->bcc = $bcc;
   return $this;
  }
 }

 /* boundary
 --------------------------------------------------------------------------*/
 public function boundary( $boundary=null )
 {
  if( is_null( $boundary ) ){
   if( strlen( $this->boundary ) <= 0 ){
    $this->boundary = md5( uniqid( rand(), true ) );
   }
   return $this->boundary;
  }
  else{
   $this->boundary = $boundary;
   return $this;
  }
 }

 /* 送信
 --------------------------------------------------------------------------*/
 public function send()
 {
  mb_language( "ja" );
  mb_internal_encoding( self::ENCODING );
  return mail(
    $this->to(),
    mb_encode_mimeheader( $this->title(), "ISO-2022-JP", "B" ),
    $this->buildBody(),
    $this->buildHeader(),
    $this->buildParam()
   );
 }

 /* 本文の構築
 --------------------------------------------------------------------------*/
 private function buildBody()
 {
  $body = mb_convert_encoding( $this->body(), 'JIS', self::ENCODING );
  if( count( $this->files() ) <= 0 ){
   return $body;
  }
  else{
   return $this->appendFiles( $body );
  }
 }

 /* ファイルを添付
 --------------------------------------------------------------------------*/
 private function appendFiles( $body )
 {
  $in_file_body = "";
  $in_file_body .= "--" . $this->boundary() . "\n";
  $in_file_body .= "Content-Type: text/plain; charset=\"iso-2022-jp\"\n";
  $in_file_body .= "Content-Transfer-Encoding: 7bit\n";
  $in_file_body .= "\n";
  $in_file_body .= $body . "\n";

  foreach( $this->files() as $file_name => $file_path ){
   if( ! file_exists( $file_path ) ){
    trigger_error( 'I was not able to attach the file', E_USER_NOTICE );
    continue;
   }

   $info   = pathinfo( $file_path );
   $content  = "application/octet-stream";
   $filename  = mb_encode_mimeheader( $file_name, "ISO-2022-JP", "B" );
   $in_file_body .= "\n";
   $in_file_body .= "--" . $this->boundary() . "\n";
   $in_file_body .= "Content-Type: " . $content . "; charset=\"iso-2022-jp\" name=\"" . $filename . "\"\n";
   $in_file_body .= "Content-Transfer-Encoding: base64\n";
   $in_file_body .= "Content-Disposition: attachment; filename=\"" . $filename . "\"\n";
   $in_file_body .= "\n";
   $in_file_body .= chunk_split( base64_encode( file_get_contents( $file_path ) ) ) . "\n";
  }
  $in_file_body .= '--' . $this->boundary() . '--';
  return $in_file_body;
 }

 /* Fromの構築
 --------------------------------------------------------------------------*/
 private function buildFrom()
 {
  $from = "";
  if( strlen( $this->name() ) <= 0 ){
   $from .= $this->from();
  }
  else{
   $from .= mb_encode_mimeheader( $this->name(), "ISO-2022-JP", "B" ) . " <" . $this->from() . ">";
  }
  return $from;
 }

 /* Ccの構築
 --------------------------------------------------------------------------*/
 private function buildCc()
 {
  $cc = "";
  if( count( $this->cc() ) > 0 ){
   $cc .= "Cc: " . implode( ",", $this->cc() ) . "\r\n";
  }
  return $cc;
 }

 /* Bccの構築
 --------------------------------------------------------------------------*/
 private function buildBcc()
 {
  $bcc = "";
  if( count( $this->bcc() ) > 0 ){
   $bcc .= "Bcc: " . implode( ",", $this->bcc() ) . "\r\n";
  }
  return $bcc;
 }

 /* ヘッダの構築
 --------------------------------------------------------------------------*/
 private function buildHeader()
 {
  $header = "";

  // デフォルト
  $header .= "X-Mailer: PHP5\r\n";
  $header .= "From: " . $this->buildFrom() . "\r\n";
  $header .= "Return-Path: " . $this->buildFrom() . "\r\n";
  $header .= $this->buildCc();
  $header .= $this->buildBcc();
  $header .= "MIME-Version: 1.0\r\n";
  $header .= "Content-Transfer-Encoding: 7bit\r\n";
  if( count( $this->files() ) <= 0 ){
   $header .= "Content-Type: text/plain; charset=\"iso-2022-jp\"\n";
  }
  else {
   $header .= "Content-Type: multipart/mixed; boundary=\"" . $this->boundary() . "\"\n";
  }

  // ユーザ定義
  $header .= $this->header();

  return $header;
 }

 /* パラメータ構築
 --------------------------------------------------------------------------*/
 private function buildParam()
 {
  $param = "";

  // デフォルト
  $param .= "-f " . $this->from();

  // ユーザ定義
  $param .= $this->param();

  return $param;
 }
}

5 Responses to [PHP]PEARを使わない添付ファイル付きメール送信クラスを公開

  1. 163行目の if( file_exists ( $path ) ){ は、合ってますか?
    ファイルが存在したらここで止まってしまうような、、

    返信削除
    返信
    1. この度はご連絡頂きまして、誠に有難うございます。
      確かに、163行目の記述が間違っていましたので、修正致しました。
      ファイルが無ければ例外を送出する処理が正しいので、
      判定にはnotを付けさせて頂きました。

      163行目 if( ! file_exists( $path ) ){

      他にも、何かバグが見つかりましたらご報告頂ければ幸いです。
      今後ともよろしくお願いいたします。

      削除
  2. とても使いやすくて、感動しました。有り難うございます!!

    返信削除
    返信
    1. ERT様

      お世話になります。
      この度はコメント頂き、誠に有難う御座います。
      そう言って頂けると、今後の励みとなります。
      有難う御座います。

      今後も、より良い情報やツールを公開していきますので、
      今後とも宜しくお願い致します。

      削除
  3. 大変有用なソースを公開していただき、ありがとうございます。

    Outlookで受信すると添付ファイルが開けない(本文中にMIMEエンコードされた文字列で表示される)という問い合わせがあって調べました。
    よくわからないのですが、Outlookでは、ヘッダ中の CR+LF をうまく処理できないみたいです。
    ソース中のmb_encode_mimeheader()の第4パラメータに"\n"を追加し、CR+LF を LFのみにして様子を見ています。

    いつもながら、マイクロ○フトさんの仕様には頭を悩まされます… orz

    返信削除

人気の投稿

Category

Algorithm (2) Android (8) ASP/aspx (1) Blogger (2) C/C++ (1) Chrome (5) CSS (9) Firefox (4) Fortran (1) Google (9) GoogleMap (2) HTML (12) IE (3) Information (4) iOS (2) iPhone/iPad/iPod (2) Java (6) JavaScript (16) jQuery (9) JSP (1) LifeRecipe (5) Linux (2) Macintosh (2) MapKit (4) Marketing (7) MySQL (3) NAMAZU (2) Objective-C (7) Other (7) Perl (1) PHP (9) Python (1) RSS/Atom (2) Ruby (1) Safari (2) SEO (11) Smarty (2) SQL (2) Tex (1) Three.js (1) Twitter (1) TwitterLog (313) UIKit (5) Unix (1) VBA/VBS (1) Windows (5) WordPress (3) Writing (5) XAMPP (1) XML (1) Yahoo (2) ZendFramework2 (14)