SMTP 프로토콜을 통해 메일을 전송

Posted by 겨울에
2011. 2. 4. 06:58 scrap/ PHP
출처 : 
PHPSchool

안녕하세요, 마크입니다.
자료실에 mymail 을 올렸는 데, 이 프로그램은 메일 수신만 하는 프로그램이어서, 메일 전송을 위한 스크립트를 따로 만들었습니다.

제 홈페이지에서 최신 버젼을 구할 수 있고, 웹메일 소스도 얻을 수 있을 것입니다.
http://gohosting.co.kr/~mark (광고는 아닙니다. -.-)

X Window 에서 작업하다 보니, 한글이 안되서 영어로 간단하게 설명한 부분도 있습니다.

만약 자세한 답변이나 부연 설명이 필요하시면 메일이나 홈페이지를 이용해 주세요. 

클래스로 만들었는 데, 간단한 설명을 할께요.

보내는 메일 서버 없이 받는 메일 서버와 PHP 스크립트가 직접 통신을 해서 메일을 전송하는 것이 주요 기능입니다.

우선 상대 (메일을 수신하는) 서버의 주소를 알아야합니다.
도메인과 메일 서버는 틀릴 수가 있습니다.
예를 들어 한메일의 경우 아이디가 mark@hanmail.net 이라면,
실제로 메일 서버는 hanmail.net 이 아니라는 것이죠.
그럼 메일 서버의 주소를 알기 위해서는 DNS 에 질의를 해서 MX 레코드를 얻어와야합니다.
한메일의 경우에는 한 10 개 정도의 MX 가 있습니다.
이 중에 아무 MX 에 메일을 전송해서는 안되고 우선 순위가 가장 높은 MX 를 골라 메일을 전송해 주어야합니다.
흔히 lowest preferrence 라고 하는 데, PHP 에는 weight 이라는 용어를 쓰더군요.
어 쨌든 이 값이 가장 낮을 것이 우선권이 높은 메일 서버가 되는 것입니다.
weight 이 모두 같다면, 맨 처음의 MX 호스트를 사용하면 되구요.

그 다음 부터는 뭐 SMTP 프로토콜을 쓰면되죠.

아래의 예제와 클래스 소스를 보시면 이해가 될 거라 생각합니다.

혹시 수정을 하시는 분은, 꼭 좀 수정한 내용 (특히, 업그레이드 된 부분^^) 을 보내 주시면 감사하겠습니다.

많은 활용을 바라며, 그럼.




예제 파일
<?
    // check on linux, check error.log
    include "SMTP.class";                                // 클래스 인클루드

    // 사용법 설명
    // 
    // SMTP.class 에는 두개의 클래스가 있다.
    // 하나는 메일 전송을 위한 SMTP 클래스, 또 다른 하나는 메일의 내용을 MIME 인코딩을 하기 위한 클래스.
    // 먼저 전송할 데이터를 MailEncode 클래스를 통해 MIME 인코딩한다.
    // 인코딩한 데이터를 SMTP 클래스를 통해 전송한다.
    

    $enc = new MailEncoder();                            // 새 객체 생성
    $enc->SetContentType("text/html");                    // 기본 마임 타입
    $enc->SetSenderMail("do41004@dreamwiz.com");        // 보내는 사람 메일 주소
    $enc->SetSenderName("Mark");                        // 보내는 사람 이름
    $enc->SetSubject("TEST");                            // 제목
    $enc->SetTo("mark@gohosting.co.kr");                // 받는 사람 메일 주소
    $enc->SetXPriority(3);                                // 메일의 중요도
    $enc->SetXMailer("MyMailer 1.0");                    // 메일러 이름. 아무거나 세팅
    $enc->SetReplyTo("do41004@dreamwiz.com");            // 받는 메일 주소
    $enc->SetBody("This is <h1>the </h1><font color=blue>body<hr>hi...");    // 내용


    // 파일을 추가하면 기본 마임 타입이 multipart/mixed 로 변경됨.
    /*
    
    // 파일 추가 예: Attach 메소드의 입력 값 (파일명, 파일 크기, 파일 내용)
    
    $path = "c:/tmp/test.prj";                    // 전송할 파일 경로
    $size = filesize($path);                    // 파일 크기
    $fr = fopen($path, "rb");                    // 내용을 읽음
    $content = fread($fr,$size);                // 
    fclose($fr);                                //
    $n = substr( strrchr($path, "/"), 1 );        // 경로를 제외한 파일명(만) 얻음
    $enc->Attach($n, $size, $content);            // 파일 추가.
    */
    
    // file attachment
    $path = "mail.sh";
    $size = filesize($path);
    $fr = fopen($path, "rb");
    $content = fread($fr,$size);
    fclose($fr);
    $n = $path; substr( strrchr($path, "/"), 1 );
    $enc->Attach($n, $size, $content);
    
    // file attachment
    $path = "core";
    $size = filesize($path);
    $fr = fopen($path, "rb");
    $content = fread($fr,$size);
    fclose($fr);
    $n = $path; substr( strrchr($path, "/"), 1 );
    $enc->Attach($n, $size, $content);
    
    
    // file attachment
    $path = "mail.txt";
    $size = filesize($path);
    $fr = fopen($path, "rb");
    $content = fread($fr,$size);
    fclose($fr);
    $n = $path; substr( strrchr($path, "/"), 1 );
    $enc->Attach($n, $size, $content);
    
    
    
    $data = $enc->GetData();                            // 인코딩된 내용을 얻는다.
                                                        // 이 내용을 SMTP 프로토콜의 DATA 요청으로 전송하면된다.

    echo "<xmp>$data</xmp>";                            // 테스트: 인코딩 된 내용을 출력해서 살펴본다.
    

    $smtp = new SMTP("do41004@hanmail.net",60,true);    // 메일을 보내기 위핸 SMTP 객체 생성
    $smtp->Log(true);                                    // 에러 기록 옵션
    
    // HELO 요청
    if ($smtp->Helo() == TEMPORARILY_REJECTED) { echo "HELO rejected"; exit; }
    // MAIL 요청
    if ($smtp->Mail("do41004@dreamwiz.com") == TEMPORARILY_REJECTED) { echo "MAIL rejected"; exit; }
    // RCPT 요청. 서버가 허락하는 한 계속 추가 가능.
    if ($smtp->Rcpt() == TEMPORARILY_REJECTED) { echo "RCPT rejected"; exit; }
    //$smtp->Rcpt("ntjaeho@gohosting.co.kr");
    // DATA 요청
    if ($smtp->Data($data) == TEMPORARILY_REJECTED) { echo "DATA rejected"; exit; }
    // QUIT 요청
    $smtp->Quit();
?>




SMTP.class 파일
<?

/***************************************************************************
                          mail.class  -  description
                             -------------------
    begin                : 2001-12-31 2:56 PM
    copyright            : (C) 2001 by Mark
    email                : do41004@dreamwiz.com
    homepage             : http://gohosting.co.kr/~mark/
    last edition         : 2001-12-31
***************************************************************************/

/***************************************************************************
*                                                                         *
*   PLEASE, give me a copy of the source that you have modified or        *
*           re-create on this.                                            *
*                                                                         *
***************************************************************************/


/*

    WARNING    :
        Since the DNS related functions are not working in Windows platform,
            the SMTP class couldn't send mails to the server which has MX records.
            But works fine on Unix like systems.
    

    NOTE    :
        The best MX has the lowest preference(weight).
        The blocking timeout should be at least 600 seconds regarding to the RFCs.
            But what about the timeout of PHP script??
        The mail format validation of the input $mail of SMTP contstructor should've done bofore.
    
        This class uses only HELO (not EHLO) when greeting to the SMTP server
        And it just quits after sending the QUIT request, no waitting for the server's response.
            This is the way how qmail does on closing the SMTP connection.
        The socket is opened in blocking mode by default.
        In funciton Helo(), the domain should be changed to real domain of my host.
        
        It is very hard to determine whether the mail rejected as temporarily or permanetly
            from the server's response. so, it guesses if the mail has rejected, the server has rejected
                as temporarily.
        
    USAGE:
        $smtp = new SMTP("mark@gohosting.co.kr");
        $smtp->Log(true);
        $smtp->Helo();
        $smtp->Mail("do41004@dreamwiz.com");
        $smtp->Rcpt();
        $smtp->Rcpt("ntjaeho@gohosting.co.kr");
        $smtp->Data("In the long history of the world");
        $smtp->Quit();
*/

define    ("ACCEPTED", 1);
define    ("TEMPORARILY_REJECTED", 2);
define    ("PERMANETLY_REJECTED", 3);

class SMTP {

    Var        $mail        = '';
    Var        $account    = '';
    Var        $host        = '';
    Var        $mx            = '';
    
    Var        $bLog        = false;    // if true, logs error message into error.log
    Var        $errno        = 0;
    Var        $errstr        = '';
    
    Var        $socket        = false;
    Var        $fatal        = false;    // if $fatal is true, then the SMTP should not proceed any further.
    
    
    /*
        The only constructor of this class.
        Gets the best MX and connects to it.
    */
    Function SMTP($mail, $timeout=600, $bLog=false)
    {
        $this->mail        = $mail;
        list ($this->account, $this->host) = split ("@",$this->mail);
        $this->mx        = $this->GetBestMx();
        $this->bLog        = $bLog=true;
        
        // 30 seconds for the timeout.
        $this->socket    = fsockopen ($this->mx, 25, &$this->$errno, &$this->errstr, 30);
        if (!$this->socket) {
            
            $this->Error("Connection To ($this->mx) Failed");
            $this->fatal=true;
        }
        // Wait for server's response.
        $res = $this->Protocol($this->Recv());
        if ( $res == ACCEPTED) {
        }
        else if ($res == TEMPORARILY_REJECTED) {
            $this->Error("temporarily reject");
            $this->fatal=true;
        }
        // Not work in some PHP build
        //if (socket_set_timeout($this->socket, $timeout, 0)) {
            //$this->Error("error: socket_set_timeout()");
        //}
        
    } // eo function
    
    /*
        Set Log option of this SMTP.
        if the input is true, then this will log.
    */
    Function Log($bOption) {
        $this->bLog=$bOption;
    }
    /*
        logs the error string
    */
    Function Error($str = "") {
        if ($str == "") $str = $this->errstr;
        //echo "<h1> $this->bLog =$bOption; $str</h1>";
        if ($this->bLog === true) {
            $fd = fopen("error.log", "a") or die ("cannot open error.log");
                fputs($fd, $str . "n");
            fclose($fd);
        }
    }

    Function Protocol($proto) {
    
        // TEST echo
        echo "RES: $proto<br>";
        $digit = substr($proto, 0, 3);
        
        switch ($digit) {
            case '220'    :
                return ACCEPTED;
            case '250'    :
            case '251'    :
                return ACCEPTED;
            case '354'    :
                return ACCEPTED;
            case '421'    :
            case '450'    :
            case '451'    :
            case '452'    :
            case '500'    :
            case '501'    :
            case '503'    :
            case '550'    :
            case '551'    :
                return TEMPORARILY_REJECTED;
                
            default        :
                return TEMPORARILY_REJECTED;
        }
    }    
    /*
        RETURN: The best MX host.
        DESC  : Gets the best MX records if the host has any.
                If there is no MX of the host, then the host itself has SMTP server.
                If all the MX hosts have the same preferrence(weight), the first mx of the returned mx list
                    will be returned.
        NOTE  :
                Every time you query for mx list to the DNS, the DNS will return some list
                    which is not the same as before. and the best MX will be changed every time.
    */
    Function GetBestMX() {
        
        // On windows !!
            //return $this->host;
        // On U(Li)nix, remove above line.
        if ( checkDNSRr ( $this->host, "MX" ) ) {
            /* If the host has MX records, get the best of them. */
            if ( getMxRr ($this->host, $MX, $weight))  {
                // Get the lowest preferrence(weight).
                $lowidx=0;
                $lowest=0;    // holds the lowest preferrence of MX
                  for ( $i = 0,$j = 1; $i < count ( $MX ); $i++,$j++ ) {
                      if ($lowest == 0) $lowest == $weight[$i];
                      if ($lowest > $weight[$i]) {
                          $lowest = $weight[$i];
                          $lowidx = $i;
                      } // eo if
                      
                      // TEST echo:
                      // echo "[preferrence=$weight[$i]] $MX[$i]<br>";
                } // eo for.
                $this->mx = $MX[$lowidx];
            }
            else {
                $this->mx = $this->host;
            }
            // eo if getMxRr
        }
        else {
            $this->mx = $this->host;
        }
        // eo if checkDSNRr
        return $this->mx;
    } // eo function


    /*
        RETURN    :
            - ACCEPTED if success.
            - TEMPORARILY_REJECTED otherwise.
    */
    Function Helo() {
        if ($this->fatal) return TEMPORARILY_REJECTED;
        
        // *mailer.com* should be changed as your host name.
        $this->Send("HELO mymailer.com");
        $res = $this->Protocol($this->Recv());
        if ($res != ACCEPTED) {
            $this->fatal = true;
            $this->Error("HELO rejected");
        }
        return $res;
    }
    /*
    
        RETURN    :
            - ACCEPTED if success.
            - TEMPORARILY_REJECTED otherwise.
    
        The input string must be sender's mail.
    */
    Function Mail($from) {
        if ($this->fatal) return TEMPORARILY_REJECTED;
        $this->Send("MAIL FROM:<$from>");
        $res = $this->Protocol($this->Recv());
        if ($res != ACCEPTED) {
            $this->fatal = true;
            $this->Error("HELO rejected");
        }
        return $res;
    }
    /*
    
        RETURN    :
            - ACCEPTED if success.
            - TEMPORARILY_REJECTED otherwise.
    
        The Input string must be bcc mail address.
        If the input string is empty, then $this->mail will be used as the first recepient mail address.
        You can add as much bcc address as the server allows.
        if this function returns TEMPORARILY_REJECTED then should stop to add bcc mails.
    */
    Function Rcpt($to="") {
        if ($this->fatal) return false;
        if ($to == "") $to = $this->mail;
        $this->Send("RCPT TO:<$to>");
        return $this->Protocol($this->Recv());
    }
    
    
    /*
        RETURN    :
            - ACCEPTED if success.
            - TEMPORARILY_REJECTED otherwise.
    */
    Function Data($data) {
        if ($this->fatal) return TEMPORARILY_REJECTED;
        $this->Send("DATA");
        if ($this->Protocol($this->Recv()) == ACCEPTED) {
            $this->Send($data);
            $this->Send(".");
            return $this->Protocol($this->Recv());
        }
        else {
            $this->fatal = true;
            $this->Error("DATA rejected");
            return TEMPORARILY_REJECTED;
        }
    }
    Function Quit() {
        if ($this->fatal) return TEMPORARILY_REJECTED;
        $this->Send("QUIT");
        // no waitting for the response!
        fclose($this->socket);
    }
    Function Send($str) {
        fputs($this->socket, $str . "rn");
    }
    // In default, this is bocking mode.
    Function Recv() {
        return fgets($this->socket, 1024);
    }

} // eo SMTP class


/*
    NOTE    :
        - the file size of Attach (...) is useless.
        
    To do:
        errors-to, bcc, cc, charset, etc...
    
*/
class MailEncoder {
    Var        $SenderMail = '';
    Var        $SenderName = '';
    Var        $Subject    = '';
    Var        $To             = '';
    Var        $XPriority    = 0;
    Var        $XMailer    = '';
    Var        $ReplyTo    = '';
    Var        $ContentType= '';
    Var        $Body        = '';
    Var        $Boundary    = '';
    Var        $Attachment = -1;
    Var        $aFileName    = array();
    Var        $aFileSize    = array();
    Var        $aFileData    = array();
    
    FUNCTION MailEncoder()
    {
    }
    FUNCTION SetSenderMail ($mail) {$this->SenderMail=$mail; }
    FUNCTION SetSenderName ($name) {$this->SenderName=$name; }
    FUNCTION SetSubject ($subject) {$this->Subject=$subject; }
    FUNCTION SetTo ($to) {$this->To=$to; }
    FUNCTION SetXPriority ($x) {$this->XPriority=$x; }
    FUNCTION SetXMailer ($x) {$this->XMailer=$x; }
    FUNCTION SetReplyTo ($x) {$this->ReplyTo=$x; }
    FUNCTION SetContentType ($x) {$this->ContentType=$x; }
    FUNCTION SetBody ($x) {$this->Body=$x; }
    /*
        If called, it does not change the contenttype now, but eventually
            the contenttype has to be chaged as "multipart/mixed".
    */
    FUNCTION Attach ($filename, $filesize, $filedata) {
        // Make a Boudary.
        if ($Boundary == '') {
            $this->Boundary = "----=_SMTPEncoder." . uniqid(getMyPid());
        }
        $this->Attachment ++;
        $this->aFileName[$this->Attachment] = $filename;
        $this->aFileSize[$this->Attachment] = $filesize;
        $this->aFileData[$this->Attachment] = $filedata;
    }
    
    FUNCTION GetData() {
        $data = "";
        $data .= "Reply-To: "$this->SenderName" <$this->ReplyTo>n";
        $data .= "From: "$this->SenderName" <$this->SenderMail>n";
        $data .= "To: <$this->To>n";
        $data .= "Subject: $this->Subjectn";
        $data .= "Date: " . date("D, d M Y H:i:s") . " +0900n";
        $data .= "MIME-Version: 1.0n";
        
        
        
        
        
        // has any attachment ??
        if ($this->Attachment != -1 ) {
            $data .= "Content-Type: multipart/mixed;n";
            $data .= "tboundary="$this->Boundary"n";
            $data .= "X-Priority: $this->XPriorityn";
            $data .= "X-Mailer: $this->XMailern";
            $data .= "n";
            $data .= "This is a multi-part message in MIME format.nn";
            
            
                // body
                $data .= "--$this->Boundaryn";
                $data .= "Content-Type: $this->ContentType;n";
                $data .= "tcharset="ks_c_5601-1987"n";
                $data .= "Content-Transfer-Encoding: base64n";
                $data .= "n";
                $data .= chunk_split(base64_encode($this->Body));
                $data .= "n";
            
            for ($i=0; $i <= $this->Attachment; $i++) {
                $n = $this->aFileName[$i];
                $s = $this->aFileSize[$i];
                $d = $this->aFileData[$i];
                $data .= "--$this->Boundaryn";
                $mime = $this->GetMime($n);
                $data .= "Content-Type: $mime;ntname="$n"n";
                $data .= "Content-Transfer-Encoding: base64n";
                $data .= "Content-Disposition: attachment;n";
                $data .= "tfilename="$n"n";
                $data .= "n";
                $data .= chunk_split(base64_encode($d));
                $data .= "n";
            }
            $data .= "--$this->Boundary--n";
        }
        else {
            
            $data .= "Content-Type: $this->ContentType;n";
            $data .= "X-Priority: $this->XPriorityn";
            $data .= "X-Mailer: $this->XMailern";
            $data .= "n" . $this->Body . "n";
        }
        return $data;
    }
    
    
    FUNCTION GetMime($n) {
        /* File extention */
        $ext = substr( strrchr($n, "."), 1 );
        
        /* MIME selection */
        switch ($ext) {
            case    'doc'    :    $mime = "application/msword"; break;
            case    'bin'    :
            case    'lha'    :
            case    'lzh'    :
            case    'exe'    :
            case    'class'    :    $mime = "application/octet-stream"; break;
            case    'pdf'    :    $mime = "application/pdf"; break;
            case    'ai'    :
            case    'eps'    :
            case    'ps'    :    $mime = "application/postscript"; break;
            case    'rtf'    :    $mime = "application/rtf"; break;
            case    'ppt'    :    $mime = "application/vnd.ms-powerpoint"; break;
            case    'dcr'    :
            case    'dir'    :
            case    'dxr'    :    $mime = "application/x-director"; break;
            case    'gtar'    :    $mime = "application/x-gtar"; break;
            case    'js'    :    $mime = "application/x-javascript"; break;
            case    'swf'    :    $mime = "application/x-shockwave-flash"; break;
            case    'tar'    :    $mime = "application/x-tar"; break;
            case    'zip'    :    $mime = "application/zip"; break;
            case    'mid'    :
            case    'midi'    :
            case    'kar'    :    $mime = "audio/midi"; break;
            case    'mpga'    :
            case    'mp2'    :
            case    'mp3'    :    $mime = "audio/mpeg"; break;
            case    'ram'    :
            case    'rm'    :    $mime = "audio/x-pn-realaudio"; break;
            case    'rpm'    :    $mime = "audio/x-pn-realaudio-plugin"; break;
            case    'ra'    :    $mime = "audio/x-realaudio"; break;
            case    'wav'    :    $mime = "audio/x-wav"; break;
            case    'gif'    :    $mime = "image/gif"; break;
            case    'jpeg'    :
            case    'jpg'    :
            case    'jpe'    :    $mime = "image/jpeg"; break;
            case    'png'    :    $mime = "image/png"; break;
            case    'pnm'    :    $mime = "image/x-portable-anymap"; break;
            case    'xbm'    :    $mime = "image/x-xbitmap"; break;
            case    'wrl'    :
            case    'vrml'    :    $mime = "model/vrml"; break;
            case    'css'    :    $mime = "text/css"; break;
            case    'asc'    :
            case    'txt'    :    $mime = "text/plain"; break;
            case    'rtf'    :    $mime = "text/rtf"; break;
            case    'rtx'    :    $mime = "text/richtext"; break;
            case    'sgml'    :
            case    'sgm'    :    $mime = "text/sgml"; break;
            case    'xml'    :    $mime = "text/xml"; break;
            case    'mpeg'    :
            case    'mpg'    :
            case    'mpe'    :    $mime = "video/mpeg"; break;
            case    'qt'    :
            case    'mov'    :    $mime = "video/quicktime"; break;
            case    'avi'    :    $mime = "video/x-msvideo"; break;
            case    'movie'    :    $mime = "video/x-sgi-movie"; break;
            case    'html'    :
            case    'htm'    :    $mime = "text/html"; break;
            default        :    $mime = "application/octet-stream";
        }
        return $mime;
    } // eo GetMime()
}

?>






'scrap >  PHP' 카테고리의 다른 글

PHP menual  (0) 2011.02.04
php - gmail의 smtp 이용 메일발송  (0) 2011.02.04