362

I am having problems understanding how to email an attachment using Python. I have successfully emailed simple messages with the smtplib. Could someone please explain how to send an attachment in an email. I know there are other posts online but as a Python beginner I find them hard to understand.

martineau
  • 119,623
  • 25
  • 170
  • 301
Richard
  • 15,152
  • 31
  • 85
  • 111
  • 6
    here's a simple implementation that can attach multiple files, and even refer to them in the case of images to embed. http://datamakessense.com/easy-scheduled-emailing-with-python-for-typical-bi-needs/ – AdrianBR Feb 08 '16 at 18:50
  • I found this useful https://www.drupal.org/project/mimemail/issues/911612 turns out image attachments need to be attached to a related type child part. If you attach the image to the root MIME part the images can show up in the attached items list, and previewed in clients like outlook365. – Hinchy Nov 29 '19 at 11:12

20 Answers20

529

Here's another:

import smtplib
from os.path import basename
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate


def send_mail(send_from, send_to, subject, text, files=None,
              server="127.0.0.1"):
    assert isinstance(send_to, list)

    msg = MIMEMultipart()
    msg['From'] = send_from
    msg['To'] = COMMASPACE.join(send_to)
    msg['Date'] = formatdate(localtime=True)
    msg['Subject'] = subject

    msg.attach(MIMEText(text))

    for f in files or []:
        with open(f, "rb") as fil:
            part = MIMEApplication(
                fil.read(),
                Name=basename(f)
            )
        # After the file is closed
        part['Content-Disposition'] = 'attachment; filename="%s"' % basename(f)
        msg.attach(part)


    smtp = smtplib.SMTP(server)
    smtp.sendmail(send_from, send_to, msg.as_string())
    smtp.close()

It's much the same as the first example... But it should be easier to drop in.

Oli
  • 235,628
  • 64
  • 220
  • 299
  • 10
    Be careful with mutable defaults: http://stackoverflow.com/questions/101268/hidden-features-of-python/113198#113198 – Gringo Suave Mar 22 '11 at 06:09
  • 1
    -1 The for loop will not work. Should read 'for file in files'. – garnertb May 16 '11 at 16:23
  • 11
    @user589983 Why not suggest an edit like any other user here would? I've changed the remaining reference to `file` into `f`. – Oli May 16 '11 at 22:26
  • 12
    Notice for Python3 developers: module "email.Utils" has been renamed to "email.utils" – gecco Nov 11 '11 at 08:11
  • 8
    for python2.5+ it's easier to use [MIMEApplication](http://docs.python.org/2/library/email.mime.html#email.mime.application.MIMEApplication) instead - reduces the first three lines of t he loop to: `part = MIMEApplication(open(f, 'rb').read())` – mata Jul 03 '13 at 12:01
  • Looks good. I just wanted to add 1 thing. I'm using procmail to filter my emails and when I encounter this email procmail had trouble figuring out the attachment name. I added the following to the attachment header and it works now: part.add_header('Content-Type', 'text/plain; name="%s"' % os.path.basename(f)) Any clarification on why this is needed for something like procmail would be appreciated. I'll see what I can find myself as well. – radtek Jun 03 '14 at 23:36
  • 2
    Certain mail clients (Thunderbird in my case) don't correctly interpret "*.txt" attachments sent via this method. To correct this, just change `MIMEBase('application', 'octet-stream')` to `MIMEBase('application', 'base64')`. Please edit the answer to reflect this. – whereswalden Jul 01 '14 at 17:37
  • 1
    in the method definition `file` should be by default equal to `None` because right now it's a pointer to same object within memory. https://stackoverflow.com/questions/6435793/optional-parameters-in-python-functions-and-their-default-values – JackLeo Oct 16 '14 at 10:52
  • 3
    The Content-dispostion don't work see http://stackoverflow.com/a/3363538/128629 or http://stackoverflow.com/a/16509278/128629 for the correct way to do it – Xavier Combelle Dec 13 '14 at 21:12
  • @XavierCombelle Have you read the docs for `MIMEApplication`? You'll see the **_params argument that takes kwargs and converts them into attachments... This works. Tested it too. Here's the source where that happens: https://github.com/python-git/python/blob/master/Lib/email/mime/multipart.py – Oli Dec 14 '14 at 10:33
  • 2
    @Oli the headers are bad: the part you missed " Additional parameters for the Content-Type header are taken from the keyword arguments (or passed into the _params argument)." for specifying a file name you want a new header – Xavier Combelle Dec 15 '14 at 21:20
  • This works for me usually but for some reason, the server disconnects sending a 834KB image. Appears to timeout. Is there any sort of "chunking" you have to do for larger attachments? Anyone else seeing this issue? – Jonathan Mar 06 '15 at 20:05
  • 1
    As others have noted, this is broken. Use the alternatives suggested by Xavier Combelle instead. Why this was accepted (an upvoted so much) is beyond me. Your attachments will be nameless!!! – John Chrysostom May 05 '15 at 19:29
  • 3
    Yes - this is incorrect. You need to pass the headers as in pcboy's answer, even on Python 2. Also, I had to pass name='foo.pdf' to MIMEApplication in addition to Content_Disposition to get the filename to register properly in my email client (Gmail). Please edit the answer! – tobias.mcnulty Aug 02 '15 at 05:58
  • @tobias.mcnulty That did it. For future reference, *you* can edit things too :) – Oli Aug 02 '15 at 15:45
  • 5
    Subject was not shown in the email sent. Worked only after changing the line to msg['Subject']=subject. I use python 2.7. – Luke Oct 29 '15 at 17:57
  • like @Luke I needed to use `msg['Subject']=subject` – Shadi Mar 11 '16 at 10:55
  • @Oli, I've edited this answer as I found it had some elements incorrect in my testing which seemed to be confirmed by the comments. While the comments and other answers here provided the solutions, it's important for this high profile (and accepted) question to be correct. Hope that's okay with you. – Peter Gibson May 27 '16 at 00:04
  • Cyrillic is broken in the body. – antonavy Aug 24 '16 at 21:02
  • I would strongly suggest to default with TLS (encrypted) instead of plaintext: ``` server = smtplib.SMTP("smtp.gmail.com", 587) server.starttls() server.ehlo() server.login(gmail_user, gmail_pwd) ``` Rest is good, thanks for submit! – N0thing Nov 21 '16 at 05:58
  • @Oli how can we add CC – Saravanan Nandhan Mar 09 '18 at 09:37
  • @SaravananNandhan Just add a `cc` header field like we did with `To`. Python will pass these headers out whatever you specify but they might not send if your MTA doesn't understand them so use [standard header fields](http://jkorpela.fi/headers.html). `msg['cc'] = COMMASPACE.join(['example1@example.com', ...])`. – Oli Mar 09 '18 at 09:44
  • 1
    @SaravananNandhan However, adding `msg['cc']` is not enough in case you really want to deliver the e-mail to them. You need to add them to the envelope too, such as; `smtp.sendmail(send_from, send_to+cc_to, msg.as_string())` considering cc_to is a list just as send_to. @Oli – highlytrainedbadger Nov 09 '20 at 08:07
  • But you don't have a body for the email. – theerrormagnet Apr 08 '21 at 06:52
  • Excellent answer – EsmaeelE Jun 19 '23 at 13:16
111

Here is the modified version from Oli for python 3

import smtplib
from pathlib import Path
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
from email import encoders


def send_mail(send_from, send_to, subject, message, files=[],
              server="localhost", port=587, username='', password='',
              use_tls=True):
    """Compose and send email with provided info and attachments.

    Args:
        send_from (str): from name
        send_to (list[str]): to name(s)
        subject (str): message title
        message (str): message body
        files (list[str]): list of file paths to be attached to email
        server (str): mail server host name
        port (int): port number
        username (str): server auth username
        password (str): server auth password
        use_tls (bool): use TLS mode
    """
    msg = MIMEMultipart()
    msg['From'] = send_from
    msg['To'] = COMMASPACE.join(send_to)
    msg['Date'] = formatdate(localtime=True)
    msg['Subject'] = subject

    msg.attach(MIMEText(message))

    for path in files:
        part = MIMEBase('application', "octet-stream")
        with open(path, 'rb') as file:
            part.set_payload(file.read())
        encoders.encode_base64(part)
        part.add_header('Content-Disposition',
                        'attachment; filename={}'.format(Path(path).name))
        msg.attach(part)

    smtp = smtplib.SMTP(server, port)
    if use_tls:
        smtp.starttls()
    smtp.login(username, password)
    smtp.sendmail(send_from, send_to, msg.as_string())
    smtp.quit()
Ehsan Iran-Nejad
  • 1,697
  • 1
  • 15
  • 20
  • thanks but it would be nice to also have the basic: the syntax for a single attached file (using it's path) – JinSnow Mar 03 '17 at 19:38
  • seems that you don't close your files, it will be garbage collected or closed at exit but it's bad habit. with open() as f: is the right way. – comte May 29 '17 at 21:26
  • Code does not work. Wrong variable name f in format(os.path.basename(f)) should be format(os.path.basename(path)) – Chris Jan 28 '18 at 22:25
  • 1
    send_to should be list[str] – Subin Dec 10 '18 at 13:07
  • 2
    best answer for me but there is a small error: replace ```import pathlib```with ```from pathlib import Path``` – AleAve81 Apr 29 '20 at 20:38
  • @AleAve81Done. Thanks for the catch – Ehsan Iran-Nejad Apr 30 '20 at 22:20
  • Code does not work. `smtplib.SMTPDataError: (550, b'your mailer sends invalid headers')` – Andrey Sokolov Jan 25 '21 at 11:47
  • 1
    Best answer so far. This should be the solution for the question. – ChiPlusPlus Jun 20 '21 at 10:09
  • Using this answer injected quotes into my attached file name. Solved by adapting requisite line to `'attachment; filename={}'` – Ruben Flam-Shepherd Jul 20 '21 at 06:05
  • 1
    @RubenFlam-Shepherd Thanks. I updated the answer and removed the quotes – Ehsan Iran-Nejad Jul 21 '21 at 16:12
  • I get "ConnectionRefusedError" when I try this. – O.rka Oct 26 '21 at 18:28
  • 1
    The `email` module in the Pyhon standard library was overhauled in Python 3.6 to be more logical, versatile, and succinct; new code should target the (no longer very) new `EmailMessage` API. Probably throw away this code and start over with modern code from [the Python `email` examples documentation.](https://docs.python.org/3/library/email.examples.html) – tripleee May 01 '22 at 10:01
  • I **believe the "most correct" is use "formatdate"** like this `msg['Date'] = formatdate(localtime=False)` (or omit "localtime") **opting for UTC time**. There is a full discussion on the subject here https://stackoverflow.com/q/10487286/3223785 . – Eduardo Lucio Jan 03 '23 at 02:57
  • I would avoid using `server.sendmail()` and instead use server.send_message(msg). Firstly, its simpler and secondly I have discovered issues with the sendmail function that I cannot explain. – Timothy C. Quinn Apr 12 '23 at 19:07
76

This is the code I ended up using:

import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email import Encoders


SUBJECT = "Email Data"

msg = MIMEMultipart()
msg['Subject'] = SUBJECT 
msg['From'] = self.EMAIL_FROM
msg['To'] = ', '.join(self.EMAIL_TO)

part = MIMEBase('application', "octet-stream")
part.set_payload(open("text.txt", "rb").read())
Encoders.encode_base64(part)
    
part.add_header('Content-Disposition', 'attachment; filename="text.txt"')

msg.attach(part)

server = smtplib.SMTP(self.EMAIL_SERVER)
server.sendmail(self.EMAIL_FROM, self.EMAIL_TO, msg.as_string())

Code is much the same as Oli's post.

Code based from Binary file email attachment problem post.

vvvvv
  • 25,404
  • 19
  • 49
  • 81
Richard
  • 15,152
  • 31
  • 85
  • 111
29
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEImage import MIMEImage
import smtplib

msg = MIMEMultipart()
msg.attach(MIMEText(file("text.txt").read()))
msg.attach(MIMEImage(file("image.png").read()))

# to send
mailer = smtplib.SMTP()
mailer.connect()
mailer.sendmail(from_, to, msg.as_string())
mailer.close()

Adapted from here.

Oli
  • 235,628
  • 64
  • 220
  • 299
  • Not quite what I am looking for. The file was sent as the body of an email. There is also missing brackets on line 6 and 7. I feel that we are getting closer though – Richard Jul 29 '10 at 13:23
  • 2
    Emails are plain text, and that's what `smtplib` supports. To send attachments, you encode them as a MIME message and send them in a plaintext email. There's a new python email module, though: http://docs.python.org/library/email.mime.html – Katriel Jul 29 '10 at 13:33
  • @katrienlalex a working example would go a long way to help my understanding – Richard Jul 29 '10 at 13:52
  • I guess I should add that I am using python 2.4 – Richard Jul 29 '10 at 13:52
  • 1
    Are you sure the above example doesn't work? I don't have a SMTP server handy, but I looked at `msg.as_string()` and it certainly looks like the body of a MIME multipart email. Wikipedia explains MIME: http://en.wikipedia.org/wiki/MIME – Katriel Jul 29 '10 at 14:04
  • @katrielalex Thanks for the resource. Even though I have a working snip of code, I still am not sure whats happening here. This should help my understanding a bit. – Richard Jul 29 '10 at 14:34
  • 1
    `Line 6, in msg.attach(MIMEText(file("text.txt").read())) NameError: name 'file' is not defined` – Benjamin2002 Aug 10 '18 at 13:56
  • i would like to send a body along with email attachment and insert table as text. Am fscing some issues. Can help me with this? https://stackoverflow.com/questions/71709672/how-to-send-a-complete-email-using-smtplib-python?noredirect=1#comment126734469_71709672 – The Great Apr 02 '22 at 02:00
13

Another way with python 3 (If someone is searching):

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

fromaddr = "sender mail address"
toaddr = "receiver mail address"

msg = MIMEMultipart()

msg['From'] = fromaddr
msg['To'] = toaddr
msg['Subject'] = "SUBJECT OF THE EMAIL"

body = "TEXT YOU WANT TO SEND"

msg.attach(MIMEText(body, 'plain'))

filename = "fileName"
attachment = open("path of file", "rb")

part = MIMEBase('application', 'octet-stream')
part.set_payload((attachment).read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', "attachment; filename= %s" % filename)

msg.attach(part)

server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login(fromaddr, "sender mail password")
text = msg.as_string()
server.sendmail(fromaddr, toaddr, text)
server.quit()

Make sure to allow “less secure apps” on your Gmail account

Sudarshan
  • 938
  • 14
  • 27
11

Gmail version, working with Python 3.6 (note that you will need to change your Gmail settings to be able to send email via smtp from it:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from os.path import basename


def send_mail(send_from: str, subject: str, text: str, 
send_to: list, files= None):

    send_to= default_address if not send_to else send_to

    msg = MIMEMultipart()
    msg['From'] = send_from
    msg['To'] = ', '.join(send_to)  
    msg['Subject'] = subject

    msg.attach(MIMEText(text))

    for f in files or []:
        with open(f, "rb") as fil: 
            ext = f.split('.')[-1:]
            attachedfile = MIMEApplication(fil.read(), _subtype = ext)
            attachedfile.add_header(
                'content-disposition', 'attachment', filename=basename(f) )
        msg.attach(attachedfile)


    smtp = smtplib.SMTP(host="smtp.gmail.com", port= 587) 
    smtp.starttls()
    smtp.login(username,password)
    smtp.sendmail(send_from, send_to, msg.as_string())
    smtp.close()

Usage:

username = 'my-address@gmail.com'
password = 'top-secret'
default_address = ['my-address2@gmail.com'] 

send_mail(send_from= username,
subject="test",
text="text",
send_to= None,
files= # pass a list with the full filepaths here...
)

To use with any other email provider, just change the smtp configurations.

Ferrarezi
  • 801
  • 10
  • 12
10

Because there are many answers here for Python 3, but none which show how to use the overhauled email library from Python 3.6, here is a quick copy+paste from the current email examples documentation. (I have abridged it somewhat to remove frills like guessing the correct MIME type.)

Modern code which targets Python >3.5 should no longer use the email.message.Message API (including the various MIMEText, MIMEMultipart, MIMEBase etc classes) or the even older mimetypes mumbo jumbo.

from email.message import EmailMessage
import smtplib

msg = EmailMessage()
msg["Subject"] = "Our family reunion"
msg["From"] = "me <sender@example.org>"
msg["To"] = "recipient <victim@example.net>"
# definitely don't mess with the .preamble

msg.set_content("Hello, victim! Look at these pictures")

with open("path/to/attachment.png", "rb") as fp:
    msg.add_attachment(
        fp.read(), maintype="image", subtype="png")

# Notice how smtplib now includes a send_message() method
with smtplib.SMTP("localhost") as s:
    s.send_message(msg)

The modern email.message.EmailMessage API is now quite a bit more versatile and logical than the older version of the library. There are still a few kinks around the presentation in the documentation (it's not obvious how to change the Content-Disposition: of an attachment, for example; and the discussion of the policy module is probably too obscure for most newcomers) and fundamentally, you still need to have some sort of idea of what the MIME structure should look like (though the library now finally takes care of a lot of the nitty-gritty around that). Perhaps see What are the "parts" in a multipart email? for a brief introduction.

Using localhost as your SMTP server obviously only works if you actually have an SMTP server running on your local computer. Properly getting email off your system is a fairly complex separate question. For simple requirements, probably use your existing email account and your provider's email server (search for examples of using port 587 with Google, Yahoo, or whatever you have - what exactly works depends somewhat on the provider; some will only support port 465, or legacy port 25 which is however now by and large impossible to use on public-facing servers because of spam filtering).

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • I very much like the idea of just using the email.message.EmailMessage API. Could you provide a code example for the function that has multiple attachments with different subtypes. – DrWhat Jun 13 '22 at 05:32
  • 2
    The linked documentation has several examples. But in brief, `for attachment, main, sub in (("path/to/image.png", "image", "png"), ("another/directory/revenge.pdf", "application", "pdf"), ("somewhere/else/abyss.xlsx", "application", "octet-stream")): with open(attachment, "rb") as fp: msg.add_attachment(fp.read(), maintype=main, subtype=sub)` (too lazy to look up the proper MIME type for XLSX; friends don't let friends use Excel anyway). – tripleee Jun 13 '22 at 05:37
  • I suggest edit it to also have `filename=...` i.e. `with open("path/to/attachment.png", "rb") as fp: msg.add_attachment( fp.read(), maintype="image", subtype="png", filename="attachment.png")` so the attachment will be downloaded with a correct name and be previewed correctly. – LWC Dec 01 '22 at 21:51
6

The simplest code I could get to is:

#for attachment email
from django.core.mail import EmailMessage

    def attachment_email(request):
            email = EmailMessage(
            'Hello', #subject
            'Body goes here', #body
            'MyEmail@MyEmail.com', #from
            ['SendTo@SendTo.com'], #to
            ['bcc@example.com'], #bcc
            reply_to=['other@example.com'],
            headers={'Message-ID': 'foo'},
            )

            email.attach_file('/my/path/file')
            email.send()

It was based on the official Django documentation

Andrade
  • 1,179
  • 12
  • 15
  • 3
    in your case you have to install django to send an email... it doesn't reply properly the question – comte May 29 '17 at 21:25
  • 1
    @comte 'coz python is only ever used for Django, right? – Auspex Nov 01 '17 at 09:21
  • 6
    @Auspex that's my point ;-) it's like installing LibreOffice to edit a config file... – comte Nov 02 '17 at 09:10
  • 1
    I find this helpful and informative. only the one module is imported, and its use is quite simple and elegant compared to the MIME hoops others jump through. In your example, by contrast, LibreOffice is more difficult to use than notepad. – 3pitt Sep 18 '19 at 17:13
  • It's so dumb that this isn't the native way of doing it. – Ian Smith Nov 30 '21 at 00:44
5

Other answers are excellent, though I still wanted to share a different approach in case someone is looking for alternatives.

Main difference here is that using this approach you can use HTML/CSS to format your message, so you can get creative and give some styling to your email. Though you aren't enforced to use HTML, you can also still use only plain text.

Notice that this function accepts sending the email to multiple recipients and also allows to attach multiple files.

I've only tried this on Python 2, but I think it should work fine on 3 as well:

import os.path
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

def send_email(subject, message, from_email, to_email=[], attachment=[]):
    """
    :param subject: email subject
    :param message: Body content of the email (string), can be HTML/CSS or plain text
    :param from_email: Email address from where the email is sent
    :param to_email: List of email recipients, example: ["a@a.com", "b@b.com"]
    :param attachment: List of attachments, exmaple: ["file1.txt", "file2.txt"]
    """
    msg = MIMEMultipart()
    msg['Subject'] = subject
    msg['From'] = from_email
    msg['To'] = ", ".join(to_email)
    msg.attach(MIMEText(message, 'html'))

    for f in attachment:
        with open(f, 'rb') as a_file:
            basename = os.path.basename(f)
            part = MIMEApplication(a_file.read(), Name=basename)

        part['Content-Disposition'] = 'attachment; filename="%s"' % basename
        msg.attach(part)

    email = smtplib.SMTP('your-smtp-host-name.com')
    email.sendmail(from_email, to_email, msg.as_string())

I hope this helps! :-)

Antony Fuentes
  • 1,013
  • 9
  • 13
3

I know this is an old question but I thought there must be an easier way of doing this than the other examples, thus I made a library that solves this cleanly without polluting your codebase. Including attachments is super easy:

from redmail import EmailSender
from pathlib import Path

# Configure an email sender
email = EmailSender(
    host="<SMTP HOST>", port=0,
    user_name="me@example.com", password="<PASSWORD>"
)

# Send an email
email.send(
    sender="me@example.com",
    receivers=["you@example.com"],
    subject="An example email"
    attachments={
        "myfile.txt": Path("path/to/a_file.txt"),
        "myfile.html": "<h1>Content of a HTML attachment</h1>"
    }
)

You may also directly attach bytes, a Pandas DataFrame (which is converted to format depending on file extension in the key), a Matplotlib Figure or a Pillow Image. The library is most likely all the features you need for an email sender (has a lot more than attachments).

To install:

pip install redmail

Use it any way you like. I also wrote extensive documentation: https://red-mail.readthedocs.io/en/latest/

miksus
  • 2,426
  • 1
  • 18
  • 34
  • `port=0` looks like a lame joke. You should expect to find one of 587, 465, or 25; but probably consult the email admin or public documentation for the service you want to use. – tripleee May 23 '22 at 09:00
  • @tripleee it's not even a joke. [Smtplib has port=0 by default](https://docs.python.org/3/library/smtplib.html#smtplib.SMTP). Regardless of your opinion about the port, how to connect to a specific SMTP server is outside of the scope of this question. – miksus May 23 '22 at 19:32
  • Huh, TIL, Thanks for setting me straight on that. I wonder what the `smtplib` documentation means by "OS default"; I presume it must be 25 everywhere? – tripleee May 24 '22 at 03:59
  • 1
    Simply brilliant. This simplifies multiple attachments and basically the whole email mess. Available in Anaconda. Bravo miksus. – DrWhat Jun 13 '22 at 06:54
2
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib
import mimetypes
import email.mime.application

smtp_ssl_host = 'smtp.gmail.com'  # smtp.mail.yahoo.com
smtp_ssl_port = 465
s = smtplib.SMTP_SSL(smtp_ssl_host, smtp_ssl_port)
s.login(email_user, email_pass)


msg = MIMEMultipart()
msg['Subject'] = 'I have a picture'
msg['From'] = email_user
msg['To'] = email_user

txt = MIMEText('I just bought a new camera.')
msg.attach(txt)

filename = 'introduction-to-algorithms-3rd-edition-sep-2010.pdf' #path to file
fo=open(filename,'rb')
attach = email.mime.application.MIMEApplication(fo.read(),_subtype="pdf")
fo.close()
attach.add_header('Content-Disposition','attachment',filename=filename)
msg.attach(attach)
s.send_message(msg)
s.quit()

For explanation, you can use this link it explains properly https://medium.com/@sdoshi579/to-send-an-email-along-with-attachment-using-smtp-7852e77623

sdoshi
  • 31
  • 4
2
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.text import MIMEText
import smtplib

msg = MIMEMultipart()

password = "password"
msg['From'] = "from_address"
msg['To'] = "to_address"
msg['Subject'] = "Attached Photo"
msg.attach(MIMEImage(file("abc.jpg").read()))
file = "file path"
fp = open(file, 'rb')
img = MIMEImage(fp.read())
fp.close()
msg.attach(img)
server = smtplib.SMTP('smtp.gmail.com: 587')
server.starttls()
server.login(msg['From'], password)
server.sendmail(msg['From'], msg['To'], msg.as_string())
server.quit()
  • 4
    hi, Welcome, Please always post an explanation of your answer when answering a question for better understanding – Ali Jan 03 '19 at 07:06
2

Here is an updated version for Python 3.6 and newer using the EmailMessage class of the overhauled email module in the Python standard library.

import mimetypes
import os
import smtplib
from email.message import EmailMessage

username = "user@example.com"
password = "password"
smtp_url = "smtp.example.com"
port = 587


def send_mail(subject: str, send_from: str, send_to: str, message: str, directory: str, filename: str):
    # Create the email message
    msg = EmailMessage()
    msg['Subject'] = subject
    msg['From'] = send_from
    msg['To'] = send_to
    # Set email content
    msg.set_content(message)

    path = directory + filename

    if os.path.exists(path):
        ctype, encoding = mimetypes.guess_type(path)
        if ctype is None or encoding is not None:
            # No guess could be made, or the file is encoded (compressed), so
            # use a generic bag-of-bits type.
            ctype = 'application/octet-stream'
        maintype, subtype = ctype.split('/', 1)
        # Add email attachment
        with open(path, 'rb') as fp:
            msg.add_attachment(fp.read(),
                           maintype=maintype,
                           subtype=subtype,
                           filename=filename)

    smtp = smtplib.SMTP(smtp_url, port)
    smtp.starttls() # for using port 587
    smtp.login(username, password)
    smtp.send_message(msg)
    smtp.quit()

You can find more examples here.

toowboga
  • 116
  • 1
  • 6
1

None of the currently given answers here will work correctly with non-ASCII symbols in filenames with clients like GMail, Outlook 2016, and others that don't support RFC 2231 (e.g., see here). The Python 3 code below is adapted from some other stackoverflow answers (sorry, didn't save the origin links) and odoo/openerp code for Python 2.7 (see ir_mail_server.py). It works correctly with GMail and others, and also uses SSL.

import smtplib, ssl
from os.path import basename
from email.mime.base import MIMEBase
from mimetypes import guess_type
from email.encoders import encode_base64
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
from email.charset import Charset


def try_coerce_ascii(string_utf8):
    """Attempts to decode the given utf8-encoded string
       as ASCII after coercing it to UTF-8, then return
       the confirmed 7-bit ASCII string.
 
       If the process fails (because the string
       contains non-ASCII characters) returns ``None``.
    """
    try:
        string_utf8.encode('ascii')
    except UnicodeEncodeError:
        return
    return string_utf8


def encode_header_param(param_text):
    """Returns an appropriate RFC 2047 encoded representation of the given
       header parameter value, suitable for direct assignation as the
       param value (e.g. via Message.set_param() or Message.add_header())
       RFC 2822 assumes that headers contain only 7-bit characters,
       so we ensure it is the case, using RFC 2047 encoding when needed.
 
       :param param_text: unicode or utf-8 encoded string with header value
       :rtype: string
       :return: if ``param_text`` represents a plain ASCII string,
                return the same 7-bit string, otherwise returns an
                ASCII string containing the RFC2047 encoded text.
    """
    if not param_text: return ""
    param_text_ascii = try_coerce_ascii(param_text)
    return param_text_ascii if param_text_ascii\
         else Charset('utf8').header_encode(param_text)


smtp_server = '<someserver.com>'
smtp_port = 465  # Default port for SSL
sender_email = '<sender_email@some.com>'
sender_password = '<PASSWORD>'
receiver_emails = ['<receiver_email_1@some.com>', '<receiver_email_2@some.com>']
subject = 'Test message'
message = """\
Hello! This is a test message with attachments.

This message is sent from Python."""

files = ['<path1>/файл1.pdf', '<path2>/файл2.png']


# Create a secure SSL context
context = ssl.create_default_context()

msg = MIMEMultipart()
msg['From'] = sender_email
msg['To'] = COMMASPACE.join(receiver_emails)
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = subject

msg.attach(MIMEText(message))

for f in files:
    mimetype, _ = guess_type(f)
    mimetype = mimetype.split('/', 1)
    with open(f, "rb") as fil:
        part = MIMEBase(mimetype[0], mimetype[1])
        part.set_payload(fil.read())
        encode_base64(part)
    filename_rfc2047 = encode_header_param(basename(f))

    # The default RFC 2231 encoding of Message.add_header() works in Thunderbird but not GMail
    # so we fix it by using RFC 2047 encoding for the filename instead.
    part.set_param('name', filename_rfc2047)
    part.add_header('Content-Disposition', 'attachment', filename=filename_rfc2047)
    msg.attach(part)

with smtplib.SMTP_SSL(smtp_server, smtp_port, context=context) as server:
    server.login(sender_email, sender_password)
    server.sendmail(sender_email, receiver_emails, msg.as_string())
  • I decided to go with this version. Specifically because of the filenames, which in my case, can contain non ASCII characters. – Cow Aug 03 '21 at 10:44
  • The updated `email` library in Python 3.6+ is quite a bit more transparent and robust around these things. It should take care of most of these details without your involvement; this is one of the primary and rather compelling reasons to migrate away from the older API you still use here. – tripleee May 23 '22 at 09:02
1

As recommended by @toowboga, if you are using Python >= 3.6, you should use email.message.EmailMessage for all emails.

Here is my version:

import os
import smtplib
from pathlib import Path as PathLib
from email.message import EmailMessage
from email.utils import formatdate as email_formatdate

class Attachment():
    Path:str = None
    Name:str = None
    MIME:str = None

    def __init__(self, path:str, mime:str, name:str=None):
        self.Path = path
        assert os.path.isfile(path), f"Attachment path not found: '{path}'"
        assert isinstance(mime, str)
        a = mime.split('/')
        assert len(a) == 2, f"Invalid mime `{mime}`. Expecting <maintype>/<subtype>"
        self.MIME = mime
        if name is None:
            self.Name = PathLib(path).name
        else:
            self.Name = name
        
    def append_to(self, msg:EmailMessage):
        assert isinstance(msg, EmailMessage)
        (_maintype, _subtype) = self.MIME.split('/')
        with open(self.Path, "rb") as f:
            msg.add_attachment(f.read(), maintype=_maintype, subtype=_subtype, filename=self.Name)
        
        

def send_mail(send_from : str, 
              to_list : list, 
              subject : str, 
              body : str, 
              cc_list : list = None,
              bcc_list : list = None, 
              attachments : list = None, 
              as_html : bool = False, 
              server : str = "127.0.0.1"):
    assert isinstance(to_list, list)

    msg = EmailMessage()
    msg['From'] = send_from
    msg['To'] = ', '.join(to_list)
    msg['Date'] = email_formatdate(localtime=True)
    msg['Subject'] = subject

    if as_html:
        msg.set_content(body, subtype='html')
    else:
        msg.set_content(body)

    if cc_list and len(cc_list) > 0:
        msg['Cc'] = ', '.join(cc_list)
    if bcc_list and len(bcc_list) > 0:
        msg['Bcc'] = ', '.join(bcc_list)

    if isinstance(attachments, list):
        for attachment in attachments:
            assert isinstance(attachment, Attachment)
            attachment.append_to(msg)

    smtp = smtplib.SMTP(server)
    smtp.send_message(msg)
    smtp.close()


# Usage:
send_mail(send_from     = "me@here.com",
          to_list       = ["you@there.com"],
          subject       = "_email_subject_",
          body          = "_email_body_",
          attachments   =[Attachment("/tmp/attachment.pdf", 'application/pdf')])
Timothy C. Quinn
  • 3,739
  • 1
  • 35
  • 47
0

Below is combination of what I've found from SoccerPlayer's post Here and the following link that made it easier for me to attach an xlsx file. Found Here

file = 'File.xlsx'
username=''
password=''
send_from = ''
send_to = 'recipient1 , recipient2'
Cc = 'recipient'
msg = MIMEMultipart()
msg['From'] = send_from
msg['To'] = send_to
msg['Cc'] = Cc
msg['Date'] = formatdate(localtime = True)
msg['Subject'] = ''
server = smtplib.SMTP('smtp.gmail.com')
port = '587'
fp = open(file, 'rb')
part = MIMEBase('application','vnd.ms-excel')
part.set_payload(fp.read())
fp.close()
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment', filename='Name File Here')
msg.attach(part)
smtp = smtplib.SMTP('smtp.gmail.com')
smtp.ehlo()
smtp.starttls()
smtp.login(username,password)
smtp.sendmail(send_from, send_to.split(',') + msg['Cc'].split(','), msg.as_string())
smtp.quit()
TonyRyan
  • 148
  • 1
  • 3
  • 8
0

With my code you can send email attachments using gmail you will need to:

Set your gmail address at ___YOUR SMTP EMAIL HERE___
Set your gmail account password at __YOUR SMTP PASSWORD HERE___
In the ___EMAIL TO RECEIVE THE MESSAGE__ part you need to set the destination email address.
Alarm notification is the subject.
Someone has entered the room, picture attached is the body.
["/home/pi/webcam.jpg"] is an image attachment.

Here is the full code:

#!/usr/bin/env python
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
from email import Encoders
import os

USERNAME = "___YOUR SMTP EMAIL HERE___"
PASSWORD = "__YOUR SMTP PASSWORD HERE___"

def sendMail(to, subject, text, files=[]):
    assert type(to)==list
    assert type(files)==list

    msg = MIMEMultipart()
    msg['From'] = USERNAME
    msg['To'] = COMMASPACE.join(to)
    msg['Date'] = formatdate(localtime=True)
    msg['Subject'] = subject

    msg.attach( MIMEText(text) )

    for file in files:
        part = MIMEBase('application', "octet-stream")
        part.set_payload( open(file,"rb").read() )
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s"'
                       % os.path.basename(file))
        msg.attach(part)

    server = smtplib.SMTP('smtp.gmail.com:587')
    server.ehlo_or_helo_if_needed()
    server.starttls()
    server.ehlo_or_helo_if_needed()
    server.login(USERNAME,PASSWORD)
    server.sendmail(USERNAME, to, msg.as_string())
    server.quit()

sendMail( ["___EMAIL TO RECEIVE THE MESSAGE__"],
        "Alarm notification",
        "Someone has entered the room, picture attached",
        ["/home/pi/webcam.jpg"] )
Xiddoc
  • 3,369
  • 3
  • 11
  • 37
John Rua
  • 1
  • 1
  • Long time no see! Good to see that you're properly attributing your code and including it directly in the answer. However, it's generally frowned upon to copy-paste the same answer code on multiple questions. If they *really* can be solved with the same solution, you should [flag the questions as duplicates](https://stackoverflow.com/help/privileges/flag-posts) instead. – Das_Geek Dec 11 '19 at 21:52
  • I would avoid using `server.sendmail()` and instead use server.send_message(msg). Firstly, its simpler and secondly I have discovered issues with the sendmail function that I cannot explain. – Timothy C. Quinn Apr 12 '23 at 19:04
0

You can also specify the type of attachment you want in your e-mail, as an example I used pdf:

def send_email_pdf_figs(path_to_pdf, subject, message, destination, password_path=None):
    ## credits: http://linuxcursor.com/python-programming/06-how-to-send-pdf-ppt-attachment-with-html-body-in-python-script
    from socket import gethostname
    #import email
    from email.mime.application import MIMEApplication
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText
    import smtplib
    import json

    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    with open(password_path) as f:
        config = json.load(f)
        server.login('me@gmail.com', config['password'])
        # Craft message (obj)
        msg = MIMEMultipart()

        message = f'{message}\nSend from Hostname: {gethostname()}'
        msg['Subject'] = subject
        msg['From'] = 'me@gmail.com'
        msg['To'] = destination
        # Insert the text to the msg going by e-mail
        msg.attach(MIMEText(message, "plain"))
        # Attach the pdf to the msg going by e-mail
        with open(path_to_pdf, "rb") as f:
            #attach = email.mime.application.MIMEApplication(f.read(),_subtype="pdf")
            attach = MIMEApplication(f.read(),_subtype="pdf")
        attach.add_header('Content-Disposition','attachment',filename=str(path_to_pdf))
        msg.attach(attach)
        # send msg
        server.send_message(msg)

inspirations/credits to: http://linuxcursor.com/python-programming/06-how-to-send-pdf-ppt-attachment-with-html-body-in-python-script

Charlie Parker
  • 5,884
  • 57
  • 198
  • 323
0

Try This i hope this might help

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
   
fromaddr = "youremailhere"
toaddr = input("Enter The Email Adress You want to send to: ")
   
# instance of MIMEMultipart
msg = MIMEMultipart()
  
# storing the senders email address  
msg['From'] = fromaddr
  
# storing the receivers email address 
msg['To'] = toaddr
  
# storing the subject 
msg['Subject'] = input("What is the Subject:\t")
# string to store the body of the mail
body = input("What is the body:\t")
  
# attach the body with the msg instance
msg.attach(MIMEText(body, 'plain'))
  
# open the file to be sent 
filename = input("filename:")
attachment = open(filename, "rb")
  
# instance of MIMEBase and named as p
p = MIMEBase('application', 'octet-stream')
  
# To change the payload into encoded form
p.set_payload((attachment).read())
  
# encode into base64
encoders.encode_base64(p)
   
p.add_header('Content-Disposition', "attachment; filename= %s" % filename)
  
# attach the instance 'p' to instance 'msg'
msg.attach(p)
  
# creates SMTP session
s = smtplib.SMTP('smtp.gmail.com', 587)
  
# start TLS for security
s.starttls()
  
# Authentication
s.login(fromaddr, "yourpaswordhere)
  
# Converts the Multipart msg into a string
text = msg.as_string()
  
# sending the mail
s.sendmail(fromaddr, toaddr, text)
  
# terminating the session
s.quit()
0

Had a bit of a hussle in getting my script to send generic attachments but after a bit of work doing research and skimming through articles on this post, I finally came up with the following

# to query:
import sys
import ast
from datetime import datetime

import smtplib
import mimetypes
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
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.text import MIMEText

from dotenv import load_dotenv, dotenv_values

load_dotenv()  # load environment variables from .env

'''
sample .env file
# .env file
SECRET_KEY="gnhfpsjxxxxxxxx"
DOMAIN="GMAIL"
TOP_LEVEL_DOMAIN="COM"
EMAIL="CHESERExxxxxx@${DOMAIN}.${TOP_LEVEL_DOMAIN}"
TO_ADDRESS = ("cheseremxxxxx@gmail.com","cheserek@gmail.com")#didn't use this in the code but you can load recipients from here
'''

import smtplib

tls_port = 587
ssl_port = 465
smtp_server_domain_names = {'GMAIL': ('smtp.gmail.com', tls_port, ssl_port),
                            'OUTLOOK': ('smtp-mail.outlook.com', tls_port, ssl_port),
                            'YAHOO': ('smtp.mail.yahoo.com', tls_port, ssl_port),
                            'AT&T': ('smtp.mail.att.net', tls_port, ssl_port),
                            }


# todo: Ability to choose mail server provider
# auto read in from the dictionary the respective mail server address and the tls and ssl ports

class Bimail:
    def __init__(self, subject, recipients):
        self.subject = subject
        self.recipients = recipients
        self.htmlbody = ''
        self.mail_username = 'will be loaded from .env file'
        self.mail_password = 'loaded from .env file as well'
        self.attachments = []

    # Creating an smtp object
    # todo: if gmail passed in use gmail's dictionary values

    def setup_mail_client(self, domain_key_to_use="GMAIL",
                          email_servers_domains_dict=smtp_server_domain_names):
        """

        :param report_pdf:
        :type to_address: str
        """
        smtpObj = None
        encryption_status = True
        config = dotenv_values(".env")
        # check if the domain_key exists from within the available email-servers-domains dict file passed in
        # else throw an error

        # read environment file to get the Domain to be used
        if f"{domain_key_to_use}" in email_servers_domains_dict.keys():
            # if the key is found do the following
            # 1.extract the domain,tls,ssl ports from email_servers dict for use in program
            try:
                values_tuple = email_servers_domains_dict.get(f"{domain_key_to_use}")
                ssl_port = values_tuple[2]
                tls_port = values_tuple[1]
                smtp_server = values_tuple[0]

                smtpObj = smtplib.SMTP(smtp_server, tls_port)
                print(f"Success connect with tls on {tls_port}")
                print('Awaiting for connection encryption via startttls()')
                encryption_status = False

            except:
                print(f"Failed connection via tls on port {tls_port}")
                try:
                    smtpObj = smtplib.SMTP_SSL(smtp_server, ssl_port)
                    print(f"Success connect with ssl on {ssl_port}")
                    encryption_status = True
                except:
                    print(f"Failed connection via ssl on port {ssl_port}")
            finally:
                print("Within Finally block")
                if not smtpObj:
                    print("Failed!!!  no Internet connection")
                else:
                    # if connection channel is unencrypted via the use of tls encrypt it
                    if not encryption_status:
                        status = smtpObj.starttls()
                        if status[0] == 220:
                            print("Successfully Encrypted tls channel")

                    print("Successfully Connected!!!! Requesting Login")
                    # Loading .env file values to config variable
                    #load Login Creds from ENV File
                    self.mail_username = f'{config.get("EMAIL")}'
                    self.mail_password = f'{cofig.get("SECRET_KEY")}'


                    status = smtpObj.login(self.mail_usernam,self.mail_password) 

                    if status[0] == 235:
                        print("Successfully Authenticated User to xxx account")
                        success = self.send(smtpObj, f'{config.get("EMAIL")}')
                        if not bool(success):
                            print(f"Success in Sending Mail to  {success}")
                            print("Disconnecting from Server INstance")
                            quit_result = smtpObj.quit()

                        else:
                            print(f"Failed to Post {success}!!!")
                            print(f"Quiting anyway !!!")
                            quit_result = smtpObj.quit()
                    else:
                        print("Application Specific Password is Required")
        else:

            print("World")

    def send(self,smtpObj,from_address):
        msg = MIMEMultipart('alternative')
        msg['From'] = from_address
        msg['Subject'] = self.subject
        msg['To'] = ", ".join(self.recipients)  # to must be array of the form ['mailsender135@gmail.com']
        msg.preamble = "preamble goes here"
        # check if there are attachments if yes, add them
        if self.attachments:
            self.attach(msg)
        # add html body after attachments
        msg.attach(MIMEText(self.htmlbody, 'html'))
        # send
        print(f"Attempting Email send to the following addresses {self.recipients}")
        result = smtpObj.sendmail(from_address, self.recipients,msg.as_string())
        return result
        

    def htmladd(self, html):
        self.htmlbody = self.htmlbody + '<p></p>' + html

    def attach(self, msg):
        for f in self.attachments:

            ctype, encoding = mimetypes.guess_type(f)
            if ctype is None or encoding is not None:
                ctype = "application/octet-stream"

            maintype, subtype = ctype.split("/", 1)

            if maintype == "text":
                fp = open(f)
                # Note: we should handle calculating the charset
                attachment = MIMEText(fp.read(), _subtype=subtype)
                fp.close()
            elif maintype == "image":
                fp = open(f, "rb")
                attachment = MIMEImage(fp.read(), _subtype=subtype)
                fp.close()

            elif maintype == "ppt":
                fp = open(f, "rb")
                attachment = MIMEApplication(fp.read(), _subtype=subtype)
                fp.close()

            elif maintype == "audio":
                fp = open(f, "rb")
                attachment = MIMEAudio(fp.read(), _subtype=subtype)
                fp.close()
            else:
                fp = open(f, "rb")
                attachment = MIMEBase(maintype, subtype)
                attachment.set_payload(fp.read())
                fp.close()
                encoders.encode_base64(attachment)
            attachment.add_header("Content-Disposition", "attachment", filename=f)
            attachment.add_header('Content-ID', '<{}>'.format(f))
            msg.attach(attachment)

    def addattach(self, files):
        self.attachments = self.attachments + files


# example below
if __name__ == '__main__':
    # subject and recipients
    mymail = Bimail('Sales email ' + datetime.now().strftime('%Y/%m/%d'),
                    ['cheseremxx@gmail.com', 'tkemboxxx@gmail.com'])
    # start html body. Here we add a greeting.
    mymail.htmladd('Good morning, find the daily summary below.')
    # Further things added to body are separated by a paragraph, so you do not need to worry about newlines for new sentences
    # here we add a line of text and an html table previously stored in the variable
    mymail.htmladd('Daily sales')
    mymail.addattach(['htmlsalestable.xlsx'])
    # another table name + table
    mymail.htmladd('Daily bestsellers')
    mymail.addattach(['htmlbestsellertable.xlsx'])
    # add image chart title
    mymail.htmladd('Weekly sales chart')
    # attach image chart
    mymail.addattach(['saleschartweekly.png'])
    # refer to image chart in html
    mymail.htmladd('<img src="cid:saleschartweekly.png"/>')
    # attach another file
    mymail.addattach(['MailSend.py'])
    # send!
    
    mymail.setup_mail_client( domain_key_to_use="GMAIL",email_servers_domains_dict=smtp_server_domain_names)
  • This code seems to be written for Python 3.5 or earlier. The `email` library was overhauled in 3.6 and is now quite a bit more versatile and logical. Probably throw away what you have and start over with the [examples from the `email` documentation.](https://docs.python.org/3/library/email.examples.html) – tripleee May 23 '22 at 08:16
  • I would avoid using `smtpObj.sendmail()` and instead use `smtpObj.send_message(msg)`. Firstly, its simpler and secondly I have discovered issues with the sendmail function that I cannot explain. – Timothy C. Quinn Apr 12 '23 at 19:06