Python Socket編程:利用SMTP發送MIME協議郵件

2017年12月31日更新

代碼更新為最新的OOP代碼,測試環境Python3.6,成功發送郵件。

Socket編程簡介

寫完程序也還是不理解什么事Socket編程,但在知乎里看到的一個問題里面的回答很不錯,這里分享一下:Socket編程簡介

SMTP簡介

SMTP(Simple Mail Transfer Protocol)即簡單郵件傳輸協議,它是一組用于由源地址到目的地址傳送郵件的規則,由它來控制信件的中轉方式。SMTP協議屬于TCP/IP協議簇,它幫助每臺計算機在發送或中轉信件時找到下一個目的地。通過SMTP協議所指定的服務器,就可以把E-mail寄到收信人的服務器上了,整個過程只要幾分鐘。SMTP服務器則是遵循SMTP協議的發送郵件服務器,用來發送或中轉發出的電子郵件
它使用由TCP提供的可靠的數據傳輸服務把郵件消息從發信人的郵件服務器傳送到收信人的郵件服務器。跟大多數應用層協議一樣,SMTP也存在兩個 端:在發信人的郵件服務器上執行的客戶端和在收信人的郵件服務器上執行的服務器端。SMTP的客戶端和服務器端同時運行在每個郵件服務器上。當一個郵件服 務器在向其他郵件服務器發送郵件消息時,它是作為SMTP客戶在運行。
SMTP協議與人們用于面對面交互的禮儀之間有許多相似之處。首先,運行在發送端郵件服務器主機上的SMTP客戶,發起建立一個到運行在接收端郵件服務 器主機上的SMTP服務器端口號25之間的TCP連接。如果接收郵件服務器當前不在工作,SMTP客戶就等待一段時間后再嘗試建立該連接。SMTP客戶和服務器先執行一些應用層握手操作。就像人們在轉手東西之前往往先自我介紹那樣,SMTP客戶和服務器也在傳送信息之前先自我介紹一下。 在這個SMTP握手階段,SMTP客戶向服務器分別指出發信人和收信人的電子郵件地址。彼此自我介紹完畢之后,客戶發出郵件消息。

MIME簡介

阮一峰的文章給了我很多靈感,具體請看MIME筆記

代碼

這里以QQ郵箱為例,之前用過126郵箱,用普通的25端口就能發送郵件,QQ郵箱則不可以,所以專門研究了下QQ郵箱

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__author__ = 'memgq'

import socket
import ssl
import base64
import time
import os
import random

class SendMail:
    __username=''
    __password=''
    __recipient=''
    msg = b'\r\n'
    endmsg = b'\r\n.\r\n'
    mailserver = ('smtp.qq.com', 465)
    heloCommand = b'HELO qq.com\r\n'
    loginCommand = b'AUTH login\r\n'
    dataCommand = b'DATA\r\n'
    quitCommand = b'QUIT\r\n'
    msgsubject = b'Subject: Test E-mail\r\n'
    msgtype = b"Content-Type: multipart/mixed;boundary='BOUNDARY'\r\n\r\n"
    msgboundary = b'--BOUNDARY\r\n'
    msgmailer = b'X-Mailer:mengqi\'s mailer\r\n'
    msgMIMI = b'MIME-Version:1.0\r\n'
    msgfileType = b"Content-type:application/octet-stream;charset=utf-8\r\n"
    msgfilename = b"Content-Disposition: attachment; filename=''\r\n"
    msgimgtype = b"Content-type:image/gif;\r\n"
    msgimgname = b"Content-Disposition: attachment; filename=''\r\n"
    msgtexthtmltype = b'Content-Type:text/html;\r\n'
    msgimgId = b'Content-ID:<test>\r\n'
    msgimgscr = b'<img src="cid:test">'
    mailcontent = ''
    __clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def login(self):
        """
        輸入用戶名和授權碼登陸QQ郵箱
        """
        print("正在發送登錄請求……")
        time.sleep(1)
        self.__sslclientSocket.send(self.loginCommand)
        recv2 = self.__sslclientSocket.recv(1024).decode('utf-8')
        if recv2[:3] !='334':
            print('登錄請求發送失敗:334 reply not received from server.')
            time.sleep(2)
            print('正在重試……')
            self.login()
        print("登錄請求發送成功……")
        self.__username = input("請輸入用戶名:")
        self.__password = input("請輸入授權碼:")
        print("正在登錄……")
        time.sleep(1)
        username = b'%s\r\n' % base64.b64encode(self.__username.encode('utf-8'))
        self.__sslclientSocket.send(username)
        recv = self.__sslclientSocket.recv(1024).decode('utf-8')
        password = b'%s\r\n' % base64.b64encode(self.__password.encode('utf-8'))
        self.__sslclientSocket.send(password)
        recv = self.__sslclientSocket.recv(1024).decode('utf-8')
        if recv[:3] != '235':
            print('登錄失敗:賬號或密碼錯誤,請使用授權碼登錄. 235 reply not received from server.',recv)
            time.sleep(2)
            print('正在重試……')
            self.login()
        print("登錄成功")
        time.sleep(1)


    def socketconnet(self):
        """
        使用socket套接字連接qq郵箱服務器,并設置ssl驗證
        """
        print("正在連接服務器……")
        time.sleep(1)
        self.__sslclientSocket = ssl.wrap_socket(self.__clientSocket, cert_reqs=ssl.CERT_NONE,
                                            ssl_version=ssl.PROTOCOL_SSLv23)
        self.__sslclientSocket.connect(self.mailserver)
        recv = self.__sslclientSocket.recv(1024).decode('utf-8')
        if recv[:3] != '220':
            print('服務器連接失敗:220 reply not received from server.')
            time.sleep(2)
            print('正在重試……')
            self.socketconnet()
        print("成功連接服務器……")
        time.sleep(1)
        print("正在請求服務器響應……")
        time.sleep(1)
        self.__sslclientSocket.send(self.heloCommand)
        recv1 = self.__sslclientSocket.recv(1024).decode('utf-8')
        if recv1[:3] != '250':
            print('服務器響應失敗:250 replay not received from server')
            time.sleep(2)
            print('正在重試……')
            self.socketconnet()
        print("成功請求服務器響應……")
        time.sleep(1)


    def sender(self):
        mailsenderCommand = b'MAIL FROM:<%s>\r\n' % self.__username.encode('utf-8')
        self.__sslclientSocket.send(mailsenderCommand)

    def recipient(self):
        self.__recipient = input("請輸入收件人郵箱:")
        time.sleep(1)
        mailrecipientCommand = b'RCPT TO:<%s>\r\n' % self.__recipient.encode('utf-8')
        self.__sslclientSocket.send(mailrecipientCommand)
        recv = self.__sslclientSocket.recv(1024).decode('utf-8')
        if recv[:3] != '250':
            print("收件人郵箱錯誤:250 replay not received from server")
            time.sleep(1)
            self.recipient()

    def senddata(self):
        self.__sslclientSocket.send(self.dataCommand)
        recv = self.__sslclientSocket.recv(1024).decode('utf-8')
        if recv[:3] != '354':
            time.sleep(1)
            self.senddata()

    def sendsubject(self):
        subject = input("請輸入郵件主題:")
        time.sleep(1)
        self.msgsubject = b'Subject: %s\r\n' % subject.encode('utf-8')
        self.__sslclientSocket.send(self.msgsubject)
        self.__sslclientSocket.send(self.msgmailer)
        self.__sslclientSocket.send(self.msgtype)
        self.__sslclientSocket.send(b'Content-Transfer-Encoding:7bit\r\n\r\n')


    def writemail(self):
        self.__sslclientSocket.send(b'\r\n\r\n' + self.msgboundary)
        self.__sslclientSocket.send(b'Content-Type: text/html;charset=utf-8\r\n')
        self.__sslclientSocket.send(b'Content-Transfer-Encoding:7bit\r\n\r\n')
        self.mailcontent=input("請輸入郵件正文:\n")
        time.sleep(1)
        self.__sslclientSocket.sendall(b'%s\r\n'%self.mailcontent.encode('utf-8'))

    def addfile(self):
        filepath=input("請輸入文件路徑:")
        time.sleep(1)
        # filepath=filepath.replace('\\','/')
        if os.path.isfile(filepath):
            filename = os.path.basename(filepath)
            #print(filename)
            time.sleep(0.1)
            self.__sslclientSocket.send(b'\r\n\r\n' + self.msgboundary)
            self.__sslclientSocket.send(self.msgfileType)
            self.msgfilename = b"Content-Disposition: attachment; filename='%s'\r\n" % filename.encode('utf-8')
            self.__sslclientSocket.send(self.msgfilename)
            #print(self.msgfilename)
            self.__sslclientSocket.send(b'Content-Transfer-Encoding:base64\r\n\r\n')
            self.__sslclientSocket.send(self.msg)
            #print(1)
            time.sleep(0.1)
            fb = open(filepath,'rb')
            while True:
                filedata = fb.read(1024)
                # print(filedata)
                if not filedata:
                    break
                self.__sslclientSocket.send(base64.b64encode(filedata))
                time.sleep(1)
            fb.close()
            #print(2)
            time.sleep(0.1)


    def addimg(self):
        self.mailcontent = input("請輸入郵件正文:")
        time.sleep(1)
        filepath = input("請輸入圖片路徑:")
        time.sleep(1)
        # filepath = filepath.replace('\\', '/')
        if os.path.isfile(filepath):
            # print(1)
            time.sleep(0.1)
            filename = os.path.basename(filepath)
            randomid = filename.split('.')[1]+str(random.randint(1000, 9999))
            # print(randomid)
            time.sleep(0.1)
            self.msgimgId = b'Content-ID:%s\r\n' % randomid.encode('utf-8')
            self.__sslclientSocket.send(b'\r\n\r\n' + self.msgboundary)
            self.__sslclientSocket.send(self.msgimgtype)
            self.__sslclientSocket.send(self.msgimgId)
            self.msgimgname = b"Content-Disposition: attachment; filename='%s'\r\n" % filename.encode('utf-8')
            self.__sslclientSocket.send(self.msgfilename)
            # print(self.msgimgId)
            time.sleep(0.1)
            self.__sslclientSocket.send(b'Content-Transfer-Encoding:base64\r\n\r\n')
            self.__sslclientSocket.send(self.msg)
            fb = open(filepath, 'rb')
            while True:
                filedata = fb.read(1024)
                # print(filedata)
                if not filedata:
                    break
                self.__sslclientSocket.send(base64.b64encode(filedata))
                time.sleep(0.1)
            fb.close()
            # print(1)
            time.sleep(0.1)
            self.__sslclientSocket.send(b'\r\n\r\n' + self.msgboundary)
            self.__sslclientSocket.send(self.msgtexthtmltype)
            self.__sslclientSocket.send(b'Content-Transfer-Encoding:8bit\r\n\r\n')
            msgimgscr = b'<img src="cid:%s">'%randomid.encode('utf-8')
            # print(1)
            time.sleep(0.1)
            self.__sslclientSocket.send(msgimgscr)
            # print(msgimgscr)
            time.sleep(0.1)
            self.__sslclientSocket.sendall(b'%s' % self.mailcontent.encode('utf-8'))
            # print(msgimgscr)
            time.sleep(0.1)

    def sendmail(self):
        # self.addimg()
        # print(1)
        # time.sleep(1)
        # self.addfile()
        # print(2)
        # self.__sslclientSocket.send(self.endmsg)
        bool_addimg = input("是否添加圖片<Y/N>:")
        bool_addfile = input("是否添加附件<Y/N>:")
        if bool_addimg.lower() == 'y':
            if bool_addfile.lower() == 'y':
                self.addimg()
                print(1)
                self.addfile()
                print(2)
                self.__sslclientSocket.send(self.endmsg)
            else:
                self.addimg()
                self.__sslclientSocket.send(self.endmsg)
        else:
            if bool_addfile.lower() == 'y':
                self.writemail()
                self.addfile()
                self.__sslclientSocket.send(self.endmsg)
            else:
                self.writemail()
                self.__sslclientSocket.send(self.endmsg)



    def quitconnect(self):
        self.__sslclientSocket.send(self.quitCommand)


if __name__ == '__main__':
    try:
        sendmail = SendMail()
        sendmail.socketconnet()
        sendmail.login()
        sendmail.sender()
        sendmail.recipient()
        sendmail.senddata()
        sendmail.sendsubject()
        sendmail.sendmail()
        time.sleep(1)
        print("發送成功!")
        sendmail.quitconnect()
    except Exception:
            print(Exception)
    finally:
        exit(0)

收獲和結論

網上幾乎沒有pythonSocket編程發送郵件的內容,也可能我沒找到,好多東西是借鑒C語言Socket編程發送郵件和基礎的MIME協議寫出來的,其實這些功能使用smtplib模塊完全可以解決,而且非常完美
我這個只是實現了基本的outlook的基本功能,但也收獲不少,貼兩張圖吧

程序運行圖
收到的郵件樣式

本次收獲如下

  • tcp/ip協議三次握手很重要,
  • 代碼執行速度比網速快的多
  • Outlook發送郵件速度慢是有原因的
  • 自己造的輪子并沒有大神造的好用(純粹為了完成作業)
  • 能不能把實驗室網速提快點
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容