Our web application sends nicely formatted messages (including some embedded images) through python's smtplib. The messages are generated with python's email.message.EmailMessage class, as shown in the code below.
At the start of October, Microsoft deprecated support for Basic Auth in their Office365 Email accounts. We had been using Basic Auth and now needed to find a new solution. After struggling with getting OAuth2 working for some time, we decided to refactor and use the Microsoft Graph API instead.
Oddly, even though most of the examples on the Graph API site includes multi-lingual (HTTP / C# / Javascript / PHP etc) examples, the one for sending an email with a MIME format (Examlpe 4) only has an HTTP example.
We would like to know if it's possible to send the email we had built using python.email.EmailMessage using the Graph API, and if yes, how to do it.
Below is some example code, showing what we did before and what we're trying to get right now.
when we run the code, we get the error
'{"error":{"code":"RequestBodyRead","message":"Requested value \'text/plain\' was not found."}}'
import smtplib
from email.utils import formatdate
from email.message import EmailMessage
import requests
server = 'smtp.office365.com' # for exampler
port = 587 # for example
from_mail = 'levrai#ninjane.er'
to_mail = 'desti#nati.on'
subject = 'Demo sending the old way!'
password = 'not_so_Secur3!'
message_parts = ['Hi sir', 'This is a demo message.', 'It could help others to help me, and possbily others too.']
# the below function builds up the nice message based on an html template
text_msg, html_msg, cids, locs = doc_mail_from_template(message_parts)
msg = EmailMessage()
msg.set_content(text_message)
msg.add_alternative(html_msg, subtype='html')
msg['From'] = from_mail
msg['To'] = to_mail
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = subject
# now embed images to the email
for loc, cid in zip(locs, cids):
with open(loc, 'rb') as img:
maintype, subtype = guess_type(img.name)[0].split('/') # know the Content-Type of the image
msg.get_payload()[1].add_related(img.read(), maintype=maintype, subtype=subtype, cid=cid) # attach it
if date_now < '2022-10-01': # before, we could do this
with smtplib.SMTP(server, port) as smtp:
smtp.starttls(context=context)
smtp.login(from_mail, password)
smtp.sendmail(from_mail, [to_mail, ], msg.as_string())
else: # now we must do this
client_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx8c5'
client_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxb96'
tenant_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx973'
userId = "levrai#ninjane.er"
authority = f"https://login.microsoftonline.com/{tenant_id}"
scopes = ["https://graph.microsoft.com/.default"]
app = msal.ConfidentialClientApplication(client_id=client_id, client_credential=client_secret, authority=authority)
result = app.acquire_token_silent(scopes, account=None)
if not result:
result = app.acquire_token_for_client(scopes=scopes)
# setup message:
email_msg = {'Message': {'Subject': subject,
'Body': {
'ContentType': 'text/plain', 'Content': e_message.as_string() }, # what do i put here?
'ToRecipients': [{'EmailAddress': {'Address': to_mail}}]
},
'SaveToSentItems': 'true'}
endpoint = f'https://graph.microsoft.com/v1.0/users/{from_user}/sendMail'
r = requests.post(endpoint, json=email_msg,
headers={'Authorization': 'Bearer ' + result['access_token'], "Content-Type": "application/json"})
You seem to be mixing in your example sending as a JSON message using the Graph with sending as a MIME message. Sending as MIME has a few restrictions eg your email has to be under 4 mb(or because of MIME bloat 2.5-3 mb). You need to make sure you encode the message correctly but the following works okay for me in phython built from EmailMessage. (the parser on the Microsoft side can be a little restrictive so start simple with your formatting and see if that works then increase the complexity of the message).
eg
from email.message import EmailMessage
import base64
import sys
import json
import logging
import msal
import requests
config = {
"authority": "https://login.microsoftonline.com/eb8db77e-65e0-4fc3-b967-.......",
"client_id": "18bb3888-dad0-4997-96b1-.........",
"scope": ["https://graph.microsoft.com/.default"],
"secret": ".............",
"tenant-id": "eb8db77e-65e0-4fc3-b967-........"
}
app = msal.ConfidentialClientApplication(config['client_id'], authority=config['authority'],
client_credential=config['secret'])
result = app.acquire_token_silent(config["scope"], account=None)
if not result:
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
result = app.acquire_token_for_client(scopes=config["scope"])
sender_email = "gscales#gbbbbb.onmicrosoft.com"
receiver_email = "gscales#ccccc.onmicrosoft.com"
def create_message(sender, to, subject, message_text):
message = EmailMessage()
message.set_content(message_text)
message['to'] = to
message['from'] = sender
message['subject'] = subject
raw = base64.urlsafe_b64encode(message.as_bytes())
return raw.decode()
messageToSend = create_message(sender_email,receiver_email,"test subject","test 123")
print(messageToSend)
endpoint = f'https://graph.microsoft.com/v1.0/users/{sender_email}/sendMail'
r = requests.post(endpoint, data=messageToSend,
headers={'Authorization': 'Bearer ' + result['access_token'], "Content-Type": "text/plain"})
print(r.status_code)
print(r.text)
Related
I have verified the following
AWS SES isn't in Sandbox. I can send email via console to non-verified email ids.
My Lambda function has a role attached with full access to SES and Lambda (since its initial basic testing gave full permissions)
The following below a basic code from AWS documentation, just hard coded my email id. Yet I can't receive any email. The lambda code runs successfully but I don't receive emails.
import json
import os
import boto3
from botocore.exceptions import ClientError
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
print('Loading function')
def lambda_handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
#print("value1 = " + event['key1'])
#print("value2 = " + event['key2'])
#print("value3 = " + event['key3'])
#return event['key1'] # Echo back the first key value
#raise Exception('Something went wrong')
SENDER = "[redacted email]"
RECIPIENT = event['email']
CONFIGURATION_SET = "ConfigSet"
AWS_REGION = "us-east-2"
SUBJECT = "Contact Us Form Details"
# The email body for recipients with non-HTML email clients.
BODY_TEXT = "Hello,\r\nPlease see the attached file for a list of customers to contact."
# The HTML body of the email.
BODY_HTML = """\
<html>
<head></head>
<body>
<h1>Hello!</h1>
<p>Please see the attached file for a list of customers to contact.</p>
</body>
</html>
"""
# The character encoding for the email.
CHARSET = "utf-8"
# Create a new SES resource and specify a region.
client = boto3.client('ses',region_name='us-east-2')
# Create a multipart/mixed parent container.
msg = MIMEMultipart('mixed')
# Add subject, from and to lines.
msg['Subject'] = "Contact Us Form Details"
msg['From'] ="[redacted email]"
msg['To'] = "[redacted email]"
# Create a multipart/alternative child container.
msg_body = MIMEMultipart('alternative')
# Encode the text and HTML content and set the character encoding. This step is
# necessary if you're sending a message with characters outside the ASCII range.
textpart = MIMEText(BODY_TEXT.encode(CHARSET), 'plain', CHARSET)
htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET)
# Add the text and HTML parts to the child container.
msg_body.attach(textpart)
msg_body.attach(htmlpart)
# Define the attachment part and encode it using MIMEApplication.
#att = MIMEApplication(open(ATTACHMENT, 'rb').read())
# Add a header to tell the email client to treat this part as an attachment,
# and to give the attachment a name.
#att.add_header('Content-Disposition','attachment',filename=os.path.basename(ATTACHMENT))
# Attach the multipart/alternative child container to the multipart/mixed
# parent container.
msg.attach(msg_body)
# Add the attachment to the parent container.
#msg.attach(att)
print(msg)
try:
#Provide the contents of the email.
response = client.send_raw_email(
Source="[redacted email]",
Destinations=[
"[redacted email]"
],
RawMessage={
'Data':msg.as_string(),
},
#ConfigurationSetName=CONFIGURATION_SET
)
# Display an error if something goes wrong.
except ClientError as e:
print(e.response['Error']['Message'])
else:
print("Email sent! Message ID:"),
print(response['MessageId'])
Attaching my cloud watch logs for reference
If your code is really what you have displayed to us, then the reason that it is not sending the email is because half your code is not being executed.
def lambda_handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
#print("value1 = " + event['key1'])
#print("value2 = " + event['key2'])
#print("value3 = " + event['key3'])
#return event['key1'] # Echo back the first key value
#raise Exception('Something went wrong')
SENDER = "[redacted email]"
RECIPIENT = event['email']
CONFIGURATION_SET = "ConfigSet"
AWS_REGION = "us-east-2"
SUBJECT = "Contact Us Form Details"
# The email body for recipients with non-HTML email clients.
BODY_TEXT = "Hello,\r\nPlease see the attached file for a list of customers to contact."
When AWS Lambda executes the function, it calls lambda_handler(). As per Python formatting, it will execute all indented lines since they form part of the function. This includes your print() statement.
However, starting with the BODY_TEXT = ... line, there is no indenting. This means that the code is part of the "main" program, and not part of the lambda_handler() function. It would be executed when the Lambda container is first instantiated, but not when the function is triggered.
Bottom line: If this is your actual code, you need to fix your indents.
If you are not getting any errors from when executing the lambda, then most probably you are not hitting the SES API. From what I see in your code, you are missing an Access Key Id and Secret Access Key. Try configuring your boto client like this:
client = boto3.client(
'ses',
region_name=region,
aws_access_key_id='aws_access_key_string',
aws_secret_access_key='aws_secret_key_string'
)
Also make sure that your lambda is deployed in the same region as your SES. I see you are using us-east-2.
Another discrepancy I see, which is also in the documentation is that in the official AWS documentation, Destinations is actually Destination. Try it without the 's'.
Can you also paste the cloudwatch logs for the lambda. I see it should print the message Id upon success. Does it?
Sometimes -- not always -- I get a name or service not known error when calling Amazon SES from Google App Engine / Python 2.7.
I'm calling Amazon's sample code, which I'll paste at the end of this question, but it's available here:
https://docs.aws.amazon.com/ses/latest/DeveloperGuide/examples-send-using-smtp.html
Of course I put in my Amazon account credentials, and use a white-listed email address to send, and a valid address to receive. The code works flawlessly, every time, when I run it on my desktop Python system.
Here is the back-trace:
File "/base/data/home/apps/m~/1.4222/emailer_test.py", line 79, in <module>
server = smtplib.SMTP(HOST, PORT)
File "/base/alloc/tmpfs/dynamic_runtimes/python27g/79cf/python27/python27_dist/lib/python2.7/smtplib.py", line 256, in __init__
(code, msg) = self.connect(host, port)
File "/base/alloc/tmpfs/dynamic_runtimes/python27g/79cf/python27/python27_dist/lib/python2.7/smtplib.py", line 316, in connect
self.sock = self._get_socket(host, port, self.timeout)
File "/base/alloc/tmpfs/dynamic_runtimes/python27g/79cf/python27/python27_dist/lib/python2.7/smtplib.py", line 291, in _get_socket
return socket.create_connection((host, port), timeout)
File "/base/alloc/tmpfs/dynamic_runtimes/python27g/79cf/python27/python27_dist/lib/python2.7/socket.py", line 560, in create_connection
for res in getaddrinfo(host, port, 0, SOCK_STREAM):
gaierror: [Errno -2] Name or service not known
I haven't figured out anything that might cause it to fail some times on App Engine and not others. This StackOverflow question talks about configuring the firewall for MailGun.
Java Google App Engine won't send email via Mailgun SMTP
I'd have to turn on billing to configure the firewall, and I don't want to unless necessary. Plus, why would it work sometimes if it were a firewall configuration error?
Here is Amazon's sample code, adapted as I use it. Also, my SES account is in the West-2 (Oregon) zone.
import smtplib
import email.utils
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# Replace sender#example.com with your "From" address.
# This address must be verified.
SENDER = 'sender#example.com'
SENDERNAME = 'Sender Name'
# Replace recipient#example.com with a "To" address. If your account
# is still in the sandbox, this address must be verified.
RECIPIENT = 'recipient#example.com'
# Replace smtp_username with your Amazon SES SMTP user name.
USERNAME_SMTP = "smtp_username"
# Replace smtp_password with your Amazon SES SMTP password.
PASSWORD_SMTP = "smtp_password"
# (Optional) the name of a configuration set to use for this message.
# If you comment out this line, you also need to remove or comment out
# the "X-SES-CONFIGURATION-SET:" header below.
CONFIGURATION_SET = "ConfigSet"
# If you're using Amazon SES in an AWS Region other than US West (Oregon),
# replace email-smtp.us-west-2.amazonaws.com with the Amazon SES SMTP
# endpoint in the appropriate region.
HOST = "email-smtp.us-west-2.amazonaws.com"
PORT = 587
# The subject line of the email.
SUBJECT = 'Amazon SES Test (Python smtplib)'
# The email body for recipients with non-HTML email clients.
BODY_TEXT = ("Amazon SES Test\r\n"
"This email was sent through the Amazon SES SMTP "
"Interface using the Python smtplib package."
)
import time
# The HTML body of the email.
BODY_HTML = """<html>
<head></head>
<body>
<h1>Amazon SES SMTP Email Test</h1>
<p>This email was sent with Amazon SES using the
<a href='https://www.python.org/'>Python</a>
<a href='https://docs.python.org/3/library/smtplib.html'>
smtplib</a> library at %s.</p>
</body>
</html>
""" % time.asctime()
# Create message container - the correct MIME type is multipart/alternative.
msg = MIMEMultipart('alternative')
msg['Subject'] = SUBJECT
msg['From'] = email.utils.formataddr((SENDERNAME, SENDER))
msg['To'] = RECIPIENT
# Comment or delete the next line if you are not using a configuration set
#msg.add_header('X-SES-CONFIGURATION-SET',CONFIGURATION_SET)
# Record the MIME types of both parts - text/plain and text/html.
part1 = MIMEText(BODY_TEXT, 'plain')
part2 = MIMEText(BODY_HTML, 'html')
# Attach parts into message container.
# According to RFC 2046, the last part of a multipart message, in this case
# the HTML message, is best and preferred.
msg.attach(part1)
msg.attach(part2)
# Try to send the message.
if 1: #try:
server = smtplib.SMTP(HOST, PORT)
server.ehlo()
server.starttls()
#stmplib docs recommend calling ehlo() before & after starttls()
server.ehlo()
server.login(USERNAME_SMTP, PASSWORD_SMTP)
server.sendmail(SENDER, RECIPIENT, msg.as_string())
server.close()
# Display an error message if something goes wrong.
"""
except Exception as e:
print ("Error: ", e)
else:
print ("Email sent!")
"""
EDIT: It seems that SMTP is not compatible with AppEngine:
Can Google App Engine use a third party SMTP server?
As you mention, there are some problems when you try to use an external SMTP service using sockets. My advice is to use the Amazon SES API as an alternative.
The request syntax for python to send an email will be like this
response = client.send_email(
Source='string',
Destination={
'ToAddresses': [
'string',
],
'CcAddresses': [
'string',
],
'BccAddresses': [
'string',
]
},
Message={
'Subject': {
'Data': 'string',
'Charset': 'string'
},
'Body': {
'Text': {
'Data': 'string',
'Charset': 'string'
},
'Html': {
'Data': 'string',
'Charset': 'string'
}
}
},
ReplyToAddresses=[
'string',
],
ReturnPath='string',
SourceArn='string',
ReturnPathArn='string',
Tags=[
{
'Name': 'string',
'Value': 'string'
},
],
ConfigurationSetName='string'
)
An example for sending a simple email:
import boto3
client = boto3.client(
'ses',
region_name=region,
aws_access_key_id='aws_access_key_string',
aws_secret_access_key='aws_secret_key_string'
)
response = client.send_email(
Destination={
'ToAddresses': ['recipient1#domain.com', 'recipient2#domain.com],
},
Message={
'Body': {
'Text': {
'Charset': 'UTF-8',
'Data': 'email body string',
},
},
'Subject': {
'Charset': 'UTF-8',
'Data': 'email subject string',
},
},
Source='sender.email#domain.com',
)
Here's more methods you can use for the SES API.
I'm having some trouble sending a message to multiple addresses using the Gmail API. I've successfully sent a message to only one address, but get the following error when I include multiple comma-separated addresses in the 'To' field:
An error occurred: <HttpError 400 when requesting
https://www.googleapis.com/gmail/v1/users/me/messages/send?alt=json
returned "Invalid to header">
I'm using the CreateMessage and SendMessage methods from this Gmail API guide:
https://developers.google.com/gmail/api/guides/sending
That guide states that the Gmail API requires messages that are RFC-2822 compliant. I again didn't have much luck using some of these addressing examples in the RFC-2822 guide:
https://www.rfc-editor.org/rfc/rfc2822#appendix-A
I'm under the impression that 'mary#x.test, jdoe#example.org, one#y.test' should be a valid string to pass into the 'to' parameter of CreateMessage, but the error that I received from SendMessage leads me to believe otherwise.
Please let me know if you can recreate this problem, or if you have any advice on where I may be making a mistake. Thank you!
Edit: Here is the actual code that yields an error...
def CreateMessage(sender, to, subject, message_text):
message = MIMEText(message_text)
message['to'] = to
message['from'] = sender
message['subject'] = subject
return {'raw': base64.urlsafe_b64encode(message.as_string())}
def SendMessage(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, error:
print 'An error occurred: %s' % error
def ComposeEmail():
# build gmail_service object using oauth credentials...
to_addr = 'Mary Smith <mary#x.test>, jdoe#example.org, Who? <60one#y.test>'
from_addr = 'me#address.com'
message = CreateMessage(from_addr,to_addr,'subject text','message body')
message = SendMessage(gmail_service,'me',message)
Getting "Invalid to header" when sending with multiple recipients (comma delimited) in a single header was a regression that was fixed on 2014-08-25.
As James says in its comment, you shouldn't waste time trying to use Gmail API when Python has excellent documented support for using SMTP : email module can compose message including attachements, and smtplib sends them. IMHO you could use Gmail API for what works out of the box but should use the robust modules form Python Standard Library when things go wrong.
It looks like you want to send a text only message : here is a solution adapted from the email module documentation and How to send email in Python via SMTPLIB from Mkyong.com:
# Import smtplib for the actual sending function
import smtplib
# Import the email modules we'll need
from email.mime.text import MIMEText
msg = MIMEText('message body')
msg['Subject'] = 'subject text'
msg['From'] = 'me#address.com'
msg['To'] = 'Mary Smith <mary#x.test>, jdoe#example.org, "Who?" <60one#y.test>'
# Send the message via Gmail SMTP server.
gmail_user = 'youruser#gmail.com'
gmail_pwd = 'yourpassword'smtpserver = smtplib.SMTP("smtp.gmail.com",587)
smtpserver = smtplib.SMTP('smtp.gmail.com')smtpserver.ehlo()
smtpserver.starttls()
smtpserver.ehlo
smtpserver.login(gmail_user, gmail_pwd)
smtpserver.send_message(msg)
smtpserver.quit()
See also User.drafts reference - error"Invalid to header"
Apparently this bug was recently introduced in Gmail API.
Email With Mandrill to multiple emailId but it only deliver to id which is first in the list to rest it does not send.I want to send mail to multiple users using mandrill API
here is my code :
class mandrillClass:
def mandrillMail(self,param):
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
msg = MIMEMultipart('alternative')
msg['Subject'] = param['subject']
msg['From'] = param['from']
msg['To'] = param['to']
html = param['message']
print html
text = 'dsdsdsdds'
part1 = MIMEText(text, 'plain')
part2 = MIMEText(html, 'html')
username = 'xyz#gmail.com'
password = 'uiyhiuyuiyhihuohohio'
msg.attach(part1)
msg.attach(part2)
s = smtplib.SMTP('smtp.mandrillapp.com', 587)
s.login(username, password)
s.sendmail(msg['From'], msg['To'], msg.as_string())
s.quit()
and here i am calling the function
from API_Program import mandrillClass
msgDic = {}
msgDic['subject'] = "testing"
msgDic['from'] = "xyz#gmail.com"
#msgDic['to'] = 'abc#gmail.com','example#gmail.com'
COMMASPACE = ', '
family = ['abc#gmail.com','example#gmail.com']
msgDic['to'] = COMMASPACE.join(family)
msgDic['message'] = "<div>soddjags</div>"
mailObj = mandrillClass()
mailObj.mandrillMail(msgDic)
Since you're using smtplib, you'll want to review the documentation for that SMTP library on how you specify multiple recipients. SMTP libraries vary in how they handle multiple recipients. Looks like this StackOverflow post has information about passing multiple recipients with smtplib: How to send email to multiple recipients using python smtplib?
You need a list instead of just strings, so something like this:
msgDic['to'] = ['abc#gmail.com','example#gmail.com']
So, your family variable is declared properly, and you shouldn't need to do anything to that.
Try:
msgDic['to'] = [{"email":fam} for fam in family]
From the docs it looks like they expect this structure:
msgDic['to'] = [ {"email":"a#b.c", "name":"a.b", "type":"to"},
{"email":"x#y.z", "name":"x.z", "type":"cc"}
]
where you can omit name and type.
I'm currently trying to add PGP signing support to my small e-mail sending script (which uses Python 3.x and python-gnupg module).
The code that signs message is:
gpg = gnupg.GPG()
basetext = basemsg.as_string().replace('\n', '\r\n')
signature = str(gpg.sign(basetext, detach=True))
if signature:
signmsg = messageFromSignature(signature)
msg = MIMEMultipart(_subtype="signed", micalg="pgp-sha1",
protocol="application/pgp-signature")
msg.attach(basemsg)
msg.attach(signmsg)
else:
print('Warning: failed to sign the message!')
(Here basemsg is of email.message.Message type.)
And messageFromSignature function is:
def messageFromSignature(signature):
message = Message()
message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
message['Content-Description'] = 'OpenPGP digital signature'
message.set_payload(signature)
return message
Then I add all the needed headers to the message (msg) and send it.
This works well for non-multipart messages, but fails when basemsg is multipart (multipart/alternative or multipart/mixed).
Manually verifying the signature against the corresponding piece of text works, but Evolution and Mutt report that the signature is bad.
Can anybody please point me to my mistake?
The problem is that Python's email.generator module doesn't add a newline before the signature part. I've reported that upstream as http://bugs.python.org/issue14983.
(The bug was fixed in Python2.7 and 3.3+ in 2014)
What is actually the MIME structure of basemsg? It appears that it has too many nested parts in it. If you export a signed message from e.g. Evolution, you'll see that it has just two parts: the body and the signature.
Here's an example which generates a message on stdout that can be read and the signature verified on both mutt (mutt -f test.mbox) and Evolution (File -> Import).
import gnupg
from email.message import Message
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
body = """
This is the original message text.
:)
"""
gpg_passphrase = "xxxx"
basemsg = MIMEText(body)
def messageFromSignature(signature):
message = Message()
message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
message['Content-Description'] = 'OpenPGP digital signature'
message.set_payload(signature)
return message
gpg = gnupg.GPG()
basetext = basemsg.as_string().replace('\n', '\r\n')
signature = str(gpg.sign(basetext, detach=True, passphrase=gpg_passphrase))
if signature:
signmsg = messageFromSignature(signature)
msg = MIMEMultipart(_subtype="signed", micalg="pgp-sha1",
protocol="application/pgp-signature")
msg.attach(basemsg)
msg.attach(signmsg)
msg['Subject'] = "Test message"
msg['From'] = "sender#example.com"
msg['To'] = "recipient#example.com"
print(msg.as_string(unixfrom=True)) # or send
else:
print('Warning: failed to sign the message!')
Note that here, I'm assuming a keyring with a passphrase, but you may not need that.
There are much more problem with the python built-in email library.
If you call the as_string procedure, the headers will be scanned for maxlinelength only in the current class, and in the childs (_payload) not! Like this:
msgRoot (You call `to_string` during sending to smtp and headers will be checked)
->msgMix (headers will be not checked for maxlinelength)
-->msgAlt (headers will be not checked for maxlinelength)
--->msgText (headers will be not checked for maxlinelength)
--->msgHtml (headers will be not checked for maxlinelength)
-->msgSign (headers will be not checked for maxlinelength)
I have signed msgMix.to_string() and then attached the signed message to the msgRoot. But during sending to the SMTP the msgMix part was different, the headers in msgMix was not chucked. Ofc, the sign was invalid.
It has taken two days for me to understand everything.. Here is my code what works and I use for sending automatic emails:
#imports
import smtplib, gnupg
from email import Charset, Encoders
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.message import Message
from email.generator import _make_boundary
#constants
EMAIL_SMTP = "localhost"
EMAIL_FROM = "Fusion Wallet <no-reply#fusionwallet.io>"
EMAIL_RETURN = "Fusion Wallet Support <support#fusionwallet.io>"
addr = 'some_target_email#gmail.com'
subject = 'test'
html = '<b>test</b>'
txt = 'test'
#character set
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
#MIME handlers
msgTEXT = MIMEText(txt, 'plain', 'UTF-8')
msgHTML = MIMEText(html, 'html', 'UTF-8')
msgRoot = MIMEMultipart(_subtype="signed", micalg="pgp-sha512", protocol="application/pgp-signature")
msgMix = MIMEMultipart('mixed')
msgAlt = MIMEMultipart('alternative')
msgSIGN = Message()
msgOWNKEY = MIMEBase('application', "octet-stream")
#Data
msgRoot.add_header('From', EMAIL_FROM)
msgRoot.add_header('To', addr)
msgRoot.add_header('Reply-To', EMAIL_FROM)
msgRoot.add_header('Reply-Path', EMAIL_RETURN)
msgRoot.add_header('Subject', subject)
msgMix.add_header('From', EMAIL_FROM)
msgMix.add_header('To', addr)
msgMix.add_header('Reply-To', EMAIL_FROM)
msgMix.add_header('Reply-Path', EMAIL_RETURN)
msgMix.add_header('Subject', subject)
msgMix.add_header('protected-headers', 'v1')
#Attach own key
ownKey = gpg.export_keys('6B6C0EBB6DC42AA4')
if ownKey:
msgOWNKEY.add_header("Content-ID", "<0x6B6C0EBB.asc>")
msgOWNKEY.add_header("Content-Disposition", "attachment", filename='0x6B6C0EBB.asc')
msgOWNKEY.set_payload(ownKey)
#Attaching
msgAlt.attach(msgTEXT)
msgAlt.attach(msgHTML)
msgMix.attach(msgAlt)
if ownKey:
msgMix.attach(msgOWNKEY)
#Sign
gpg = gnupg.GPG()
msgSIGN.add_header('Content-Type', 'application/pgp-signature; name="signature.asc"')
msgSIGN.add_header('Content-Description', 'OpenPGP digital signature')
msgSIGN.add_header("Content-Disposition", "attachment", filename='signature.asc')
originalSign = gpg.sign(msgMix.as_string().replace('\n', '\r\n').strip()).data
spos = originalSign.index('-----BEGIN PGP SIGNATURE-----')
sign = originalSign[spos:]
msgSIGN.set_payload(sign)
#Create new boundary
msgRoot.set_boundary(_make_boundary(msgMix.as_string()))
#Set the payload
msgRoot.set_payload(
"--%(boundary)s\n%(mix)s--%(boundary)s\n%(sign)s\n--%(boundary)s--\n" % {
'boundary':msgRoot.get_boundary(),
'mix':msgMix.as_string(),
'sign':msgSIGN.as_string(),
}
)
#Send to SMTP
s = smtplib.SMTP(EMAIL_SMTP)
s.sendmail(EMAIL_FROM, addr, msgRoot.as_string())
s.quit()