Environment: Ubuntu 18.10, Python 2.7.15, Django 1.11.16
I'm trying to send an email containing an inline image. I have the following code:
msg = EmailMultiAlternatives(some_subject, some_body, 'from#some-domain.com', ['to#some#domain'])
img_data = open('path/to/image.png', 'rb').read()
img = MIMEImage(img_data)
msg.attach(img)
msg.send()
(I've only included the code that I think is relevant but I can add more on demand.)
The above properly works and the image is properly displayed on most of the email clients (about 7 of them, both mobile, desktop or webmail ones) that I tested on, with two exceptions: Mozilla Thunderbird 60 and some macOS native email client.
On Thunderbird the image is not displayed inline but at the very end of the message. On the macOS client, the image is displayed inline but additionally it is also displayed at the very end of the message.
I composed and sent a test message from another email client, containing an inline image which was properly displayed on both Thunderbird and macOS. I compared the headers of this message with the headers of the message generated by my code.
I noticed that the faulty message has the 'Content-Type' set to 'multipart/mixed' while the properly displayed message had the same header set to 'multipart/related'.
I saved the faulty message in an eml file and manually changed the value of that header and then loaded the message in Thunderbird. The message was properly displayed and the image was in the right place.
If I could set that header to the proper value, the problem would be solved.
So, my question is: is there any possibility to tell EmailMultiAlternatives to set 'Content-Type' : 'multipart/related' instead of the default value of 'multipart/mixed'?
I tried to add the header like this but it is not working:
msg = EmailMultiAlternatives(some_subject, some_body, 'from#some-domain.com', ['to#some#domain'], headers={'Content-Type' : 'multipart/related'})
I got the following error ( I use Amazon SES):
400 Bad Request
<ErrorResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
<Error>
<Type>Sender</Type>
<Code>InvalidParameterValue</Code>
<Message>Duplicate header 'Content-Type'.</Message>
</Error>
<RequestId>xxxxxxxxxx</RequestId>
</ErrorResponse>
If I can't modify that header, do you suggest any alternatives?
If you look at the source code, you'll see that EmailMultiAlternatives is a subclass of EmailMessage, which itself has a class attribute:
mixed_subtype = 'mixed'
So if you create your own subclass to override this, you should get what you need:
class EmailMultiAlternativesRelated(EmailMultiAlternatives):
mixed_subtype = 'related'
That's it, now you just use this new class, and it will use "multipart/related".
(the _create_attachments() method passes this subtype to python's SafeMIMEMultipart which creates the actual headers for each attachment.)
Related
I am trying to send a text message that contains both text and a hypeprlink but am encountering the following message from the Twilio API:
"Error - 12300 Invalid Content-Type: Attempt to retrieve MediaUrl returned an unsupported Content-Type."
Here is the code I am attempting to leverage:
import os
from twilio.rest import Client
# Find your Account SID and Auth Token at twilio.com/console
# and set the environment variables. See http://twil.io/secure
account_sid = os.environ['TWILIO_ACCOUNT_SID']
auth_token = os.environ['TWILIO_AUTH_TOKEN']
client = Client(account_sid, auth_token)
message = client.messages \
.create(
body='Article: https://4r7s.short.gy/GPaoh7',
from_='123-345-5667',
to='123-345-5668',
)
When I send a message without a hyperlink it works fine (e.g. body = 'Here is the article for you to read') but when it contains a link I receive the aforementioned error. I've also tried using a shortened url of the above but that causes the same issue.
I was just able to send messages containing that exact link using my own Twilio account.
There might be an issue in that you are using phone numbers in local format, when they should be provided in e.164 format.
It's possible that your message is being blocked. Certain carriers don't like when you use link shorteners to obscure a link.
The error you are getting definitely seems weird, since you are not sending media. If you continue to have issues with this, I would contact Twilio support.
I've tried with no conclusions to resend emails with Python.
Once I've logged in SMTP and IMAP with TLS, this is what I have written:
status, data = self._imapserver.fetch(id, "(RFC822)")
email_data = data[0][1]
# create a Message instance from the email data
message = email.message_from_string(email_data)
# replace headers (could do other processing here)
message.replace_header("From", 'blablabla#bliblibli.com')
message.replace_header("To", 'blobloblo#blublublu.com')
self._smtpserver.sendmail('blablabla#bliblibli.com', 'blobloblo#blublublu.com', message.as_string())
But the problem is that the variable data doesn't catch the information from the email, even if the ID is the one I need.
It tells me:
b'The specified message set is invalid.'
How can I transfer an email with Python?
Like the error message says, whatever you have in id is invalid. We don't know what you put there, so all we can tell you is what's already in the error message.
(Also, probably don't use id as a variable name, as you will shadow the built-in function with the same name.)
There are additional bugs further on in your code; you need to use message_from_bytes if you want to parse it, though there is really no need to replace the headers just to resend it.
status, data = self._imapserver.fetch(correct_id, "(RFC822)")
self._smtpserver.sendmail('blablabla#bliblibli.com', 'blobloblo#blublublu.com', data[0][1])
If you want to parse the message, you should perhaps add a policy argument; this selects the modern EmailMessage API which was introduced in Python 3.6.
from email.policy import default
...
message = email.message_from_bytes(data[0][1], policy=default)
message["From"] = "blablabla#bliblibli.com"
message["To"] = "blobloblo#blublublu.com"
self._smtpserver.send_message(message)
The send_message method is an addition to the new API. If the message could contain other recipient headers like Cc:, Bcc: etc, perhaps using the good old sendmail method would be better, as it ignores the message's headers entirely.
I am working on a GAE(Google App Engine) based python app and which have sendgrid python SDK(v3.2.10) integrated into it. What I am trying do is right now that whenever sendgrid pushes an event webhook of type "bounce" I want to delete that bounced email from the list of bounced emails present on sendgrid.
I have already gone through the documentation provided on the official site. First I tried to delete email address using SDK and it worked fine on localhost. But after deploying it to the live server it just doesn't do anything and falls in the exception clause.
Code snippet:
try:
send_grid_client = sendgrid.SendGridAPIClient(apikey=SENDGRID_API_KEY)
data = {"emails": [email.strip()]}
delete_response = send_grid_client.client.suppression.bounces.delete(
request_body=data)
except Exception as exception:
logging.info('Exception is: {}'.format(exception))
pass
As it did not work as expected, I am now trying to do the same using REST API.
Code snippet:
import requests
data = {"emails": [email]}
headers = {"Authorization": "Bearer {}".format(SENDGRID_API_KEY)}
delete_response = requests.delete("https://api.sendgrid.com/v3/suppression/bounces", data=json.dumps(data), headers=headers)
logging.info(delete_response)
logging.info(delete_response.status_code)
logging.info(delete_response.text)
Now, sendgrid API is continuously returning error 400 with message {"errors":[{"field":null,"message":"emails or delete_all params required"}]}. I simply could not figure out how to overcome this issue. Maybe I am missing how to pass request body in the delete function but, I could not figure it out.
I just figured out the issue.
It's the SendGrid API docs here which causes confusion as it is not mentioned clearly that they have a different way of calling the same endpoint when you want to delete a single email address or list of emails.
For a single email, it needs to be passed in the URL i.e. https://api.sendgrid.com/v3/suppression/bounces/{email_address}
For a list of emails, the list needs to be passed in the body of the delete request. i.e. it will look like this {"emails": [email_address_1, email_address_1, ...]}
As in the question above a single email was meant to be deleted and it was being passed as {"emails": [email_address_1]} in the delete request. Sendgrid API was not able to digest this info and was throwing an error. The email address was to be passed in the URL.
This issue has been resolved. But, I wonder why Sendgrid API was not able to digest this info {"emails": [email_address_1]}. Why they have a hard assumption that list will always have elements greater than one in it.
In Django EmailMultiAlternatives documentation there is nothing about how to add headers like "format" or "Reply-To" in EmailMultiAlternatives. It took a while for me to figure it out and I am sending this post to help others with saving their time.
As you can see in django's source code, EmailMultiAlternatives inherits from EmailMessage, so they take the same parameters in the init constructor. This way, we can add headers like:
msg = EmailMultiAlternatives(
subject, message, from_email, to_list,
headers={'Reply-To': "email#example.com", 'format': 'flowed'}
)
Back in 2015 OP complained, that there were no instructions in documentation, how to add headers such as "Format" and "Reply-To" in Django mail (django.core.mail) module. However today, while using same documentation link. We can find description and examples in 2018 easily:
class EmailMessage[source]
The EmailMessage class is initialized with the following parameters
(in the given order, if positional arguments are used). All parameters
are optional and can be set at any time prior to calling the send()
method.
subject: The subject line of the email.
body: The body text. This should be a plain text message.
from_email: The sender’s address. Both fred#example.com and Fred <fred#example.com> forms are legal. If omitted, the DEFAULT_FROM_EMAIL
setting is used.
to: A list or tuple of recipient addresses.
bcc: A list or tuple of addresses used in the “Bcc” header when sending the email.
connection: An email backend instance. Use this parameter if you want to use the same connection for multiple messages. If omitted, a
new connection is created when send() is called.
attachments: A list of attachments to put on the message. These can be either email.MIMEBase.MIMEBase instances, or (filename, content, mimetype) triples.
headers: A dictionary of extra headers to put on the message. The keys are the header name, values are the header values. It’s up to the
caller to ensure header names and values are in the correct format for
an email message. The corresponding attribute is extra_headers.
cc: A list or tuple of recipient addresses used in the “Cc” header when sending the email.
For example:
email = EmailMessage('Hello', 'Body goes here', 'from#example.com',
['to1#example.com', 'to2#example.com'], ['bcc#example.com'],
headers = {'Reply-To': 'another#example.com', 'format': 'flowed'})
As we see from examples, EmailMessage has headers argument (dictionary) too, EmailMultiAlternatives according to docstring in source code is:
A version of EmailMessage that makes it easy to send multipart/alternative
messages. For example, including text and HTML versions of the text is
made easier.
So if you don't need something specific, EmailMessage is fine, because currently EmailMultiAlternatives is for easy inclusion of text and HTML versions of text.
I am trying to create a set on Quizlet.com, using its API found here: https://quizlet.com/api/2.0/docs/sets#add
Here is my code of a set I am trying to create:
import requests
quizkey = my_client_id
authcode = my_secret_code # I'm not sure if I need this or not
data = {"client_id":quizkey, "whitespace":1, "title":"my-api-set",
"lang_terms":"it", "lang_definitions":"en",
"terms":['uno','due'], "definitions":["one","two"]}
apiPrefix = "https://api.quizlet.com/2.0/sets"
r = requests.post(url=apiPrefix, params=data)
print r.text
The response is:
{
"http_code": 401,
"error": "invalid_scope",
"error_title": "Not Allowed",
"error_description": "You do not have sufficient permissions to perform the requested action."
}
I also tried "access_token":authcode instead of "client_id":quizkey, but this resulted in the error: "You do not have sufficient permissions to perform the requested action."
How can I fix this and not get a 401 error?
Alright so 3 and a half years later (!!) I've looked into this again and here's what I've discovered.
To add a set you need an access token - this is different to the client_id (what I call quizkey in my code), and to be quite honest I don't remember what authcode in my code is.
This token is obtained by going through the user authentication flow. To summarise it:
Send a POST request to https://quizlet.com/authorize like so:
https://quizlet.com/authorize?response_type=code&client_id=MY_CLIENT_ID&scope=read&state=RANDOM_STRING
Keep the response_type as code, replace client_id with your client_id, keep the scope as read, and state can be anything
I believe this requires human intervention because you're literally authorising your own account? Not sure of another way...
You'll receive a response back with a code
Let's call this RESPONSE_CODE for now
Send a POST request to https://api.quizlet.com/oauth/token, specifying 4 mandatory parameters:
grant_type="authorization_code" (this never changes)
code=RESPONSE_CODE
redirect_uri=https://yourredirecturi.com (this can be found at your personal API dashboard)
client ID and secret token separated by a colon and then base64-encoded (the user authentication flow link above tells you what this is if you don't want to do any of the encoding)
You'll receive the access_token from this API call
Now you can use that access_token in your call to create a set like I've done above (just replace "client_id":quizkey with "access_token":access_token)
You will need to authenticate in order to make sets. This link gives an overview:
https://quizlet.com/api/2.0/docs/making_api_calls
And this one provides details about the authentication process:
https://quizlet.com/api/2.0/docs/authorization_code_flow