Welcome, guest | Sign In | My Account | Store | Cart

Using code gleaned from the net and from my own brain I created this convenient wrapper to send email messages via SMTP in Python. This class allows you to send plain text email messages or HTML encoded messages. You can also add attachments to the messages.

Python, 222 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
from email import encoders
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import mimetypes
import os
import re
import smtplib

class Email:
    """
    This class handles the creation and sending of email messages
    via SMTP.  This class also handles attachments and can send
    HTML messages.  The code comes from various places around
    the net and from my own brain.
    """
    def __init__(self, smtpServer):
        """
        Create a new empty email message object.

        @param smtpServer: The address of the SMTP server
        @type smtpServer: String
        """
        self._textBody = None
        self._htmlBody = None
        self._subject = ""
        self._smtpServer = smtpServer
        self._reEmail = re.compile("^([\\w \\._]+\\<[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\>|[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$")
        self.clearRecipients()
        self.clearAttachments()
    
    def send(self):
        """
        Send the email message represented by this object.
        """
        # Validate message
        if self._textBody is None and self._htmlBody is None:
            raise Exception("Error! Must specify at least one body type (HTML or Text)")
        if len(self._to) == 0:
            raise Exception("Must specify at least one recipient")
        
        # Create the message part
        if self._textBody is not None and self._htmlBody is None:
            msg = MIMEText(self._textBody, "plain")
        elif self._textBody is None and self._htmlBody is not None:
            msg = MIMEText(self._htmlBody, "html")
        else:
            msg = MIMEMultipart("alternative")
            msg.attach(MIMEText(self._textBody, "plain"))
            msg.attach(MIMEText(self._htmlBody, "html"))
        # Add attachments, if any
        if len(self._attach) != 0:
            tmpmsg = msg
            msg = MIMEMultipart()
            msg.attach(tmpmsg)
        for fname,attachname in self._attach:
            if not os.path.exists(fname):
                print "File '%s' does not exist.  Not attaching to email." % fname
                continue
            if not os.path.isfile(fname):
                print "Attachment '%s' is not a file.  Not attaching to email." % fname
                continue
            # Guess at encoding type
            ctype, encoding = mimetypes.guess_type(fname)
            if ctype is None or encoding is not None:
                # No guess could be made so use a binary type.
                ctype = 'application/octet-stream'
            maintype, subtype = ctype.split('/', 1)
            if maintype == 'text':
                fp = open(fname)
                attach = MIMEText(fp.read(), _subtype=subtype)
                fp.close()
            elif maintype == 'image':
                fp = open(fname, 'rb')
                attach = MIMEImage(fp.read(), _subtype=subtype)
                fp.close()
            elif maintype == 'audio':
                fp = open(fname, 'rb')
                attach = MIMEAudio(fp.read(), _subtype=subtype)
                fp.close()
            else:
                fp = open(fname, 'rb')
                attach = MIMEBase(maintype, subtype)
                attach.set_payload(fp.read())
                fp.close()
                # Encode the payload using Base64
                encoders.encode_base64(attach)
            # Set the filename parameter
            if attachname is None:
                filename = os.path.basename(fname)
            else:
                filename = attachname
            attach.add_header('Content-Disposition', 'attachment', filename=filename)
            msg.attach(attach)
        # Some header stuff
        msg['Subject'] = self._subject
        msg['From'] = self._from
        msg['To'] = ", ".join(self._to)
        msg.preamble = "You need a MIME enabled mail reader to see this message"
        # Send message
        msg = msg.as_string()
        server = smtplib.SMTP(self._smtpServer)
        server.sendmail(self._from, self._to, msg)
        server.quit()
    
    def setSubject(self, subject):
        """
        Set the subject of the email message.
        """
        self._subject = subject
    
    def setFrom(self, address):
        """
        Set the email sender.
        """
        if not self.validateEmailAddress(address):
            raise Exception("Invalid email address '%s'" % address)
        self._from = address
    
    def clearRecipients(self):
        """
        Remove all currently defined recipients for
        the email message.
        """
        self._to = []
    
    def addRecipient(self, address):
        """
        Add a new recipient to the email message.
        """
        if not self.validateEmailAddress(address):
            raise Exception("Invalid email address '%s'" % address)
        self._to.append(address)
    
    def setTextBody(self, body):
        """
        Set the plain text body of the email message.
        """
        self._textBody = body
    
    def setHtmlBody(self, body):
        """
        Set the HTML portion of the email message.
        """
        self._htmlBody = body
    
    def clearAttachments(self):
        """
        Remove all file attachments.
        """
        self._attach = []
    
    def addAttachment(self, fname, attachname=None):
        """
        Add a file attachment to this email message.

        @param fname: The full path and file name of the file
                      to attach.
        @type fname: String
        @param attachname: This will be the name of the file in
                           the email message if set.  If not set
                           then the filename will be taken from
                           the fname parameter above.
        @type attachname: String
        """
        if fname is None:
            return
        self._attach.append( (fname, attachname) )
    
    def validateEmailAddress(self, address):
        """
        Validate the specified email address.
        
        @return: True if valid, False otherwise
        @rtype: Boolean
        """
        if self._reEmail.search(address) is None:
            return False
        return True
    
if __name__ == "__main__":
    # Run some tests
    mFrom = "Test User <test@mydomain.com>"
    mTo = "you@yourdomain.com"
    m = Email("mail.mydomain.com")
    m.setFrom(mFrom)
    m.addRecipient(mTo)
    
    # Simple Plain Text Email
    m.setSubject("Plain text email")
    m.setTextBody("This is a plain text email <b>I should not be bold</b>")
    m.send()
    
    # Plain text + attachment
    m.setSubject("Text plus attachment")
    m.addAttachment("/home/user/image.png")
    m.send()

    # Simple HTML Email
    m.clearAttachments()
    m.setSubject("HTML Email")
    m.setTextBody(None)
    m.setHtmlBody("The following should be <b>bold</b>")
    m.send()

    # HTML + attachment
    m.setSubject("HTML plus attachment")
    m.addAttachment("/home/user/image.png")
    m.send()
    
    # Text + HTML
    m.clearAttachments()
    m.setSubject("Text and HTML Message")
    m.setTextBody("You should not see this text in a MIME aware reader")
    m.send()
    
    # Text + HTML + attachment
    m.setSubject("HTML + Text + attachment")
    m.addAttachment("/home/user/image.png")
    m.send()

6 comments

John Barham 13 years, 11 months ago  # | flag

Very nice!

I'm using this with Python 2.4 where I had to change the email imports to:

import email.Encoders as encoders
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEImage import MIMEImage
from email.MIMEAudio import MIMEAudio
Jim Eggleston 13 years, 8 months ago  # | flag

Thanks for this recipe, exactly what I was looking for. I added a parameter to the addAttachment method to allow a mime type to be specified for each attachment. Also added code to the send method to handle application mimetypes with email.mime.application.MIMEApplication. The changes probably don't make that much difference in practice but it was easy enough to add them.

Daniel Reis 11 years, 2 months ago  # | flag

Great recipe, but it needs fixing: I found that some mail clients may not see the attachment.

It needs to be a MIMEMultipart('related') message, including a MIMEMultipart('alternative') section, after which attachment sections can be added, an in this other recipe.

Glenn 11 years, 2 months ago  # | flag

Daniel, what needs to be fixed? It looks like if you supply both a text and html part that they will get put into an alternative subsection of another Multipart... lines 55-57 do that. Are you saying that line 56 should pass a subtype parameter of 'related' rather than defaulting to 'mixed'? I guess that should be the case.

Gus E 11 years, 1 month ago  # | flag

This version doesn't accept any emails with upper case characters. I had too change line 79 to if self._reEmail.search(address, re.IGNORECASE) is None: for the code to work

naveen tikeriha 7 years, 11 months ago  # | flag

Thanks for this great recipe. It exactly matched my requirement. Code worked as is to fulfill my needs. Its very easily extendable to fit in other formats.