2016-05-13 190 views
12

用gmail和python发送电子邮件的推荐方式是什么?通过gmail和python发送电子邮件

有很多,所以线程的,但大多数是老也与用户名密码&不工作更多的SMTP或用户有降级自己的Gmail的安全性(例如,参见here)。

OAuth是推荐的方式吗?

+0

你是否使用过“python gmail oauth library”? – Selcuk

+1

在[Gmail Python API快速入门](https://developers.google.com/gmail/api/quickstart/python)中,我没有看到任何使用oauth2 – davedwards

+4

提升此问题的弱化安全提及,因为谷歌文档过剩掩盖了解决方案的最终简单性。寻找合适答案的研究并不明显,也不简单。 此外,@Selcuk,只是谷歌搜索“python oauth库”不是一个答案,或者甚至从设计答案中删除一步。 – Andrew

回答

19

答案显示了如何使用gmail API和python发送电子邮件。还更新了用附件发送电子邮件的答案。

Gmail API & OAuth - >不需要在脚本中保存用户名和密码。

脚本第一次打开浏览器授权脚本并将在本地存储凭据(它不会存储用户名和密码)。随后的运行不需要浏览器,并可以直接发送电子邮件。

有了这个方法,你不会得到像下面SMTPException错误,没有必要让安全性较低的应用访问:

raise SMTPException("SMTP AUTH extension not supported by server.") 
smtplib.SMTPException: SMTP AUTH extension not supported by server. 


下面是使用Gmail API发送电子邮件的步骤:

Turn on Gmail API steps (向导链接here,更多信息here

第2步:安装谷歌客户端库

pip install --upgrade google-api-python-client 

第3步:使用下面的脚本来发送电子邮件(只是改变在主函数中的变量)

import httplib2 
import os 
import oauth2client 
from oauth2client import client, tools 
import base64 
from email.mime.multipart import MIMEMultipart 
from email.mime.text import MIMEText 
from apiclient import errors, discovery 
import mimetypes 
from email.mime.image import MIMEImage 
from email.mime.audio import MIMEAudio 
from email.mime.base import MIMEBase 

SCOPES = 'https://www.googleapis.com/auth/gmail.send' 
CLIENT_SECRET_FILE = 'client_secret.json' 
APPLICATION_NAME = 'Gmail API Python Send Email' 

def get_credentials(): 
    home_dir = os.path.expanduser('~') 
    credential_dir = os.path.join(home_dir, '.credentials') 
    if not os.path.exists(credential_dir): 
     os.makedirs(credential_dir) 
    credential_path = os.path.join(credential_dir, 
            'gmail-python-email-send.json') 
    store = oauth2client.file.Storage(credential_path) 
    credentials = store.get() 
    if not credentials or credentials.invalid: 
     flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) 
     flow.user_agent = APPLICATION_NAME 
     credentials = tools.run_flow(flow, store) 
     print('Storing credentials to ' + credential_path) 
    return credentials 

def SendMessage(sender, to, subject, msgHtml, msgPlain, attachmentFile=None): 
    credentials = get_credentials() 
    http = credentials.authorize(httplib2.Http()) 
    service = discovery.build('gmail', 'v1', http=http) 
    if attachmentFile: 
     message1 = createMessageWithAttachment(sender, to, subject, msgHtml, msgPlain, attachmentFile) 
    else: 
     message1 = CreateMessageHtml(sender, to, subject, msgHtml, msgPlain) 
    result = SendMessageInternal(service, "me", message1) 
    return result 

def SendMessageInternal(service, user_id, message): 
    try: 
     message = (service.users().messages().send(userId=user_id, body=message).execute()) 
     print('Message Id: %s' % message['id']) 
     return message 
    except errors.HttpError as error: 
     print('An error occurred: %s' % error) 
     return "Error" 
    return "OK" 

def CreateMessageHtml(sender, to, subject, msgHtml, msgPlain): 
    msg = MIMEMultipart('alternative') 
    msg['Subject'] = subject 
    msg['From'] = sender 
    msg['To'] = to 
    msg.attach(MIMEText(msgPlain, 'plain')) 
    msg.attach(MIMEText(msgHtml, 'html')) 
    return {'raw': base64.urlsafe_b64encode(msg.as_string())} 

def createMessageWithAttachment(
    sender, to, subject, msgHtml, msgPlain, attachmentFile): 
    """Create a message for an email. 

    Args: 
     sender: Email address of the sender. 
     to: Email address of the receiver. 
     subject: The subject of the email message. 
     msgHtml: Html message to be sent 
     msgPlain: Alternative plain text message for older email clients   
     attachmentFile: The path to the file to be attached. 

    Returns: 
     An object containing a base64url encoded email object. 
    """ 
    message = MIMEMultipart('mixed') 
    message['to'] = to 
    message['from'] = sender 
    message['subject'] = subject 

    messageA = MIMEMultipart('alternative') 
    messageR = MIMEMultipart('related') 

    messageR.attach(MIMEText(msgHtml, 'html')) 
    messageA.attach(MIMEText(msgPlain, 'plain')) 
    messageA.attach(messageR) 

    message.attach(messageA) 

    print("create_message_with_attachment: file: %s" % attachmentFile) 
    content_type, encoding = mimetypes.guess_type(attachmentFile) 

    if content_type is None or encoding is not None: 
     content_type = 'application/octet-stream' 
    main_type, sub_type = content_type.split('/', 1) 
    if main_type == 'text': 
     fp = open(attachmentFile, 'rb') 
     msg = MIMEText(fp.read(), _subtype=sub_type) 
     fp.close() 
    elif main_type == 'image': 
     fp = open(attachmentFile, 'rb') 
     msg = MIMEImage(fp.read(), _subtype=sub_type) 
     fp.close() 
    elif main_type == 'audio': 
     fp = open(attachmentFile, 'rb') 
     msg = MIMEAudio(fp.read(), _subtype=sub_type) 
     fp.close() 
    else: 
     fp = open(attachmentFile, 'rb') 
     msg = MIMEBase(main_type, sub_type) 
     msg.set_payload(fp.read()) 
     fp.close() 
    filename = os.path.basename(attachmentFile) 
    msg.add_header('Content-Disposition', 'attachment', filename=filename) 
    message.attach(msg) 

    return {'raw': base64.urlsafe_b64encode(message.as_string())} 


def main(): 
    to = "[email protected]" 
    sender = "[email protected]" 
    subject = "subject" 
    msgHtml = "Hi<br/>Html Email" 
    msgPlain = "Hi\nPlain Email" 
    SendMessage(sender, to, subject, msgHtml, msgPlain) 
    # Send message with attachment: 
    SendMessage(sender, to, subject, msgHtml, msgPlain, '/path/to/file.pdf') 

if __name__ == '__main__': 
    main() 

提示为在linux上运行这个代码,不带浏览器:
如果你的linux环境没有浏览器来完成第一次授权过程,你可以运行一次在你的笔记本电脑上的代码(Mac或Windows),然后将凭据复制到目标Linux机器上。凭证通常存储在以下目标:

~/.credentials/gmail-python-email-send.json 
+0

谢谢!我不明白你为什么将send()分隔成SendMessage()和SendMessageInternal(),但我猜如果你这样做了,有一个原因。你能解释一下为什么? – JinSnow

+0

这是有多种原因的。首先使其更具可读性。 @Guillaume SendMessageInternal的内容与gmail api内部相关,我不觉得它需要在SendMessage功能中可见。第二,在运行电子邮件期间,SendMessageInternal是瓶颈,错误等在该函数中显示了99%的时间。因此,当SendMessageInternal失败时,它会在日志中更明显。希望澄清。 – apadana

+0

非常感谢,现在有道理!顺便说一句,因为你的答案是迄今为止最好的解释我们如何使用gmail api发送电子邮件的方法(比google tuto好得多),那么包括一个额外的内容也是很好的:附件(给出的单个文件由它的路径)。有几个关于它的线程,但他们专注于更复杂的问题,使他们难以理解(并在您的代码中实现,重点在于要点) – JinSnow

10

我修改这个如下与Python3工作,通过Python Gmail API 'not JSON serializable'

import httplib2 
import os 
import oauth2client 
from oauth2client import client, tools 
import base64 
from email.mime.multipart import MIMEMultipart 
from email.mime.text import MIMEText 
from apiclient import errors, discovery 

SCOPES = 'https://www.googleapis.com/auth/gmail.send' 
CLIENT_SECRET_FILE = 'client_secret.json' 
APPLICATION_NAME = 'Gmail API Python Send Email' 

def get_credentials(): 
    home_dir = os.path.expanduser('~') 
    credential_dir = os.path.join(home_dir, '.credentials') 
    if not os.path.exists(credential_dir): 
     os.makedirs(credential_dir) 
    credential_path = os.path.join(credential_dir, 'gmail-python-email-send.json') 
    store = oauth2client.file.Storage(credential_path) 
    credentials = store.get() 
    if not credentials or credentials.invalid: 
     flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) 
     flow.user_agent = APPLICATION_NAME 
     credentials = tools.run_flow(flow, store) 
     print('Storing credentials to ' + credential_path) 
    return credentials 

def SendMessage(sender, to, subject, msgHtml, msgPlain): 
    credentials = get_credentials() 
    http = credentials.authorize(httplib2.Http()) 
    service = discovery.build('gmail', 'v1', http=http) 
    message1 = CreateMessage(sender, to, subject, msgHtml, msgPlain) 
    SendMessageInternal(service, "me", message1) 

def SendMessageInternal(service, user_id, message): 
    try: 
     message = (service.users().messages().send(userId=user_id, body=message).execute()) 
     print('Message Id: %s' % message['id']) 
     return message 
    except errors.HttpError as error: 
     print('An error occurred: %s' % error) 

def CreateMessage(sender, to, subject, msgHtml, msgPlain): 
    msg = MIMEMultipart('alternative') 
    msg['Subject'] = subject 
    msg['From'] = sender 
    msg['To'] = to 
    msg.attach(MIMEText(msgPlain, 'plain')) 
    msg.attach(MIMEText(msgHtml, 'html')) 
    raw = base64.urlsafe_b64encode(msg.as_bytes()) 
    raw = raw.decode() 
    body = {'raw': raw} 
    return body 

def main(): 
    to = "[email protected]" 
    sender = "[email protected]" 
    subject = "subject" 
    msgHtml = "Hi<br/>Html Email" 
    msgPlain = "Hi\nPlain Email" 
    SendMessage(sender, to, subject, msgHtml, msgPlain) 

if __name__ == '__main__': 
    main() 
+1

这几乎为我工作。我不得不使用Quickstart示例代替'get_credentials()'函数:https://developers.google.com/gmail/api/quickstart/python(加上一些全局代码)。我得到一个错误:'TypeError:run_flow()缺少1个需要的位置参数:'flags''与此代码。但这是我发现的更完整的例子之一。 – Sobigen

+0

感谢这个链接,我做了以下改动:'return {'raw':base64.urlsafe_b64encode(message.as_bytes())}'''raw = base64.urlsafe_b64encode(message.as_bytes())\ raw = raw.decode()\ body = {'raw':raw} \ return body'和'if main_type =='text':'from'rb'flag(read binary)to'r'(read text)。享受 – ioanb7

3

这里启发需要Python的3.6代码(和解释)发送没有(或附有)附件的电子邮件。

(要带附件发送刚刚取消对2线波纹管## without attachment和评论的2线波纹管## with attachment

所有信贷(和赞成票),以Apadana酒店

import httplib2 
import os 
import oauth2client 
from oauth2client import client, tools 
import base64 
from email import encoders 

#needed for attachment 
import smtplib 
import mimetypes 
from email import encoders 
from email.message import Message 
from email.mime.audio import MIMEAudio 
from email.mime.base import MIMEBase 
from email.mime.image import MIMEImage 
from email.mime.multipart import MIMEMultipart 
from email.mime.text import MIMEText 
from email.mime.application import MIMEApplication 
#List of all mimetype per extension: http://help.dottoro.com/lapuadlp.php or http://mime.ritey.com/ 

from apiclient import errors, discovery #needed for gmail service 




## About credentials 
# There are 2 types of "credentials": 
#  the one created and downloaded from https://console.developers.google.com/apis/ (let's call it the client_id) 
#  the one that will be created from the downloaded client_id (let's call it credentials, it will be store in C:\Users\user\.credentials) 


     #Getting the CLIENT_ID 
      # 1) enable the api you need on https://console.developers.google.com/apis/ 
      # 2) download the .json file (this is the CLIENT_ID) 
      # 3) save the CLIENT_ID in same folder as your script.py 
      # 4) update the CLIENT_SECRET_FILE (in the code below) with the CLIENT_ID filename 


     #Optional 
     # If you don't change the permission ("scope"): 
      #the CLIENT_ID could be deleted after creating the credential (after the first run) 

     # If you need to change the scope: 
      # you will need the CLIENT_ID each time to create a new credential that contains the new scope. 
      # Set a new credentials_path for the new credential (because it's another file) 
def get_credentials(): 
    # If needed create folder for credential 
    home_dir = os.path.expanduser('~') #>> C:\Users\Me 
    credential_dir = os.path.join(home_dir, '.credentials') # >>C:\Users\Me\.credentials (it's a folder) 
    if not os.path.exists(credential_dir): 
     os.makedirs(credential_dir) #create folder if doesnt exist 
    credential_path = os.path.join(credential_dir, 'cred send mail.json') 

    #Store the credential 
    store = oauth2client.file.Storage(credential_path) 
    credentials = store.get() 

    if not credentials or credentials.invalid: 
     CLIENT_SECRET_FILE = 'client_id to send Gmail.json' 
     APPLICATION_NAME = 'Gmail API Python Send Email' 
     #The scope URL for read/write access to a user's calendar data 

     SCOPES = 'https://www.googleapis.com/auth/gmail.send' 

     # Create a flow object. (it assists with OAuth 2.0 steps to get user authorization + credentials) 
     flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) 
     flow.user_agent = APPLICATION_NAME 

     credentials = tools.run_flow(flow, store) 

    return credentials 




## Get creds, prepare message and send it 
def create_message_and_send(sender, to, subject, message_text_plain, message_text_html, attached_file): 
    credentials = get_credentials() 

    # Create an httplib2.Http object to handle our HTTP requests, and authorize it using credentials.authorize() 
    http = httplib2.Http() 

    # http is the authorized httplib2.Http() 
    http = credentials.authorize(http)  #or: http = credentials.authorize(httplib2.Http()) 

    service = discovery.build('gmail', 'v1', http=http) 

    ## without attachment 
    message_without_attachment = create_message_without_attachment(sender, to, subject, message_text_html, message_text_plain) 
    send_Message_without_attachement(service, "me", message_without_attachment, message_text_plain) 


    ## with attachment 
    # message_with_attachment = create_Message_with_attachment(sender, to, subject, message_text_plain, message_text_html, attached_file) 
    # send_Message_with_attachement(service, "me", message_with_attachment, message_text_plain,attached_file) 

def create_message_without_attachment (sender, to, subject, message_text_html, message_text_plain): 
    #Create message container 
    message = MIMEMultipart('alternative') # needed for both plain & HTML (the MIME type is multipart/alternative) 
    message['Subject'] = subject 
    message['From'] = sender 
    message['To'] = to 

    #Create the body of the message (a plain-text and an HTML version) 
    message.attach(MIMEText(message_text_plain, 'plain')) 
    message.attach(MIMEText(message_text_html, 'html')) 

    raw_message_no_attachment = base64.urlsafe_b64encode(message.as_bytes()) 
    raw_message_no_attachment = raw_message_no_attachment.decode() 
    body = {'raw': raw_message_no_attachment} 
    return body 



def create_Message_with_attachment(sender, to, subject, message_text_plain, message_text_html, attached_file): 
    """Create a message for an email. 

    message_text: The text of the email message. 
    attached_file: The path to the file to be attached. 

    Returns: 
    An object containing a base64url encoded email object. 
    """ 

    ##An email is composed of 3 part : 
     #part 1: create the message container using a dictionary { to, from, subject } 
     #part 2: attach the message_text with .attach() (could be plain and/or html) 
     #part 3(optional): an attachment added with .attach() 

    ## Part 1 
    message = MIMEMultipart() #when alternative: no attach, but only plain_text 
    message['to'] = to 
    message['from'] = sender 
    message['subject'] = subject 

    ## Part 2 (the message_text) 
    # The order count: the first (html) will be use for email, the second will be attached (unless you comment it) 
    message.attach(MIMEText(message_text_html, 'html')) 
    message.attach(MIMEText(message_text_plain, 'plain')) 

    ## Part 3 (attachement) 
    # # to attach a text file you containing "test" you would do: 
    # # message.attach(MIMEText("test", 'plain')) 

    #-----About MimeTypes: 
    # It tells gmail which application it should use to read the attachement (it acts like an extension for windows). 
    # If you dont provide it, you just wont be able to read the attachement (eg. a text) within gmail. You'll have to download it to read it (windows will know how to read it with it's extension). 

    #-----3.1 get MimeType of attachment 
     #option 1: if you want to attach the same file just specify it’s mime types 

     #option 2: if you want to attach any file use mimetypes.guess_type(attached_file) 

    my_mimetype, encoding = mimetypes.guess_type(attached_file) 

    # If the extension is not recognized it will return: (None, None) 
    # If it's an .mp3, it will return: (audio/mp3, None) (None is for the encoding) 
    #for unrecognized extension it set my_mimetypes to 'application/octet-stream' (so it won't return None again). 
    if my_mimetype is None or encoding is not None: 
     my_mimetype = 'application/octet-stream' 


    main_type, sub_type = my_mimetype.split('/', 1)# split only at the first '/' 
    # if my_mimetype is audio/mp3: main_type=audio sub_type=mp3 

    #-----3.2 creating the attachement 
     #you don't really "attach" the file but you attach a variable that contains the "binary content" of the file you want to attach 

     #option 1: use MIMEBase for all my_mimetype (cf below) - this is the easiest one to understand 
     #option 2: use the specific MIME (ex for .mp3 = MIMEAudio) - it's a shorcut version of MIMEBase 

    #this part is used to tell how the file should be read and stored (r, or rb, etc.) 
    if main_type == 'text': 
     print("text") 
     temp = open(attached_file, 'r') # 'rb' will send this error: 'bytes' object has no attribute 'encode' 
     attachement = MIMEText(temp.read(), _subtype=sub_type) 
     temp.close() 

    elif main_type == 'image': 
     print("image") 
     temp = open(attached_file, 'rb') 
     attachement = MIMEImage(temp.read(), _subtype=sub_type) 
     temp.close() 

    elif main_type == 'audio': 
     print("audio") 
     temp = open(attached_file, 'rb') 
     attachement = MIMEAudio(temp.read(), _subtype=sub_type) 
     temp.close()    

    elif main_type == 'application' and sub_type == 'pdf': 
     temp = open(attached_file, 'rb') 
     attachement = MIMEApplication(temp.read(), _subtype=sub_type) 
     temp.close() 

    else:        
     attachement = MIMEBase(main_type, sub_type) 
     temp = open(attached_file, 'rb') 
     attachement.set_payload(temp.read()) 
     temp.close() 

    #-----3.3 encode the attachment, add a header and attach it to the message 
    encoders.encode_base64(attachement) #https://docs.python.org/3/library/email-examples.html 
    filename = os.path.basename(attached_file) 
    attachement.add_header('Content-Disposition', 'attachment', filename=filename) # name preview in email 
    message.attach(attachement) 


    ## Part 4 encode the message (the message should be in bytes) 
    message_as_bytes = message.as_bytes() # the message should converted from string to bytes. 
    message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) #encode in base64 (printable letters coding) 
    raw = message_as_base64.decode() # need to JSON serializable (no idea what does it means) 
    return {'raw': raw} 



def send_Message_without_attachement(service, user_id, body, message_text_plain): 
    try: 
     message_sent = (service.users().messages().send(userId=user_id, body=body).execute()) 
     message_id = message_sent['id'] 
     # print(attached_file) 
     print (f'Message sent (without attachment) \n\n Message Id: {message_id}\n\n Message:\n\n {message_text_plain}') 
     # return body 
    except errors.HttpError as error: 
     print (f'An error occurred: {error}') 




def send_Message_with_attachement(service, user_id, message_with_attachment, message_text_plain, attached_file): 
    """Send an email message. 

    Args: 
    service: Authorized Gmail API service instance. 
    user_id: User's email address. The special value "me" can be used to indicate the authenticated user. 
    message: Message to be sent. 

    Returns: 
    Sent Message. 
    """ 
    try: 
     message_sent = (service.users().messages().send(userId=user_id, body=message_with_attachment).execute()) 
     message_id = message_sent['id'] 
     # print(attached_file) 

     # return message_sent 
    except errors.HttpError as error: 
     print (f'An error occurred: {error}') 


def main(): 
    to = "[email protected]" 
    sender = "[email protected]" 
    subject = "subject test1" 
    message_text_html = r'Hi<br/>Html <b>hello</b>' 
    message_text_plain = "Hi\nPlain Email" 
    attached_file = r'C:\Users\Me\Desktop\audio.m4a' 
    create_message_and_send(sender, to, subject, message_text_plain, message_text_html, attached_file) 


if __name__ == '__main__': 
     main() 
+1

嗨,通过这个我可以发送附件的邮件。但它附加了一个名为'noname.txt'的附加附件。我怎样才能删除它? – RaThOd

+1

@RaTHOd对不起,我前一段时间写过这个,我记得这个问题,我想我没有设法解决它。如果你找到解决方案,请纠正它。 – JinSnow

1

感谢,@Guillame ,@apadana。 @ Guillaume的答案在Win/Python3.7中对我很好,但只做了一处改变。对于所有3条打印语句,我不得不删除了“F”,在变化:

print (f'An error occurred: {error}') 

print ('An error occurred: {error}') 

也期待在@ apandana的回答,让您client_secret的第一部分.json文件。对我来说这更明确。