[2] | 1 | #! /usr/bin/env python
|
---|
| 2 |
|
---|
| 3 | '''SMTP/ESMTP client class.
|
---|
| 4 |
|
---|
| 5 | This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTP
|
---|
| 6 | Authentication) and RFC 2487 (Secure SMTP over TLS).
|
---|
| 7 |
|
---|
| 8 | Notes:
|
---|
| 9 |
|
---|
| 10 | Please remember, when doing ESMTP, that the names of the SMTP service
|
---|
| 11 | extensions are NOT the same thing as the option keywords for the RCPT
|
---|
| 12 | and MAIL commands!
|
---|
| 13 |
|
---|
| 14 | Example:
|
---|
| 15 |
|
---|
| 16 | >>> import smtplib
|
---|
| 17 | >>> s=smtplib.SMTP("localhost")
|
---|
| 18 | >>> print s.help()
|
---|
| 19 | This is Sendmail version 8.8.4
|
---|
| 20 | Topics:
|
---|
| 21 | HELO EHLO MAIL RCPT DATA
|
---|
| 22 | RSET NOOP QUIT HELP VRFY
|
---|
| 23 | EXPN VERB ETRN DSN
|
---|
| 24 | For more info use "HELP <topic>".
|
---|
| 25 | To report bugs in the implementation send email to
|
---|
| 26 | sendmail-bugs@sendmail.org.
|
---|
| 27 | For local information send email to Postmaster at your site.
|
---|
| 28 | End of HELP info
|
---|
| 29 | >>> s.putcmd("vrfy","someone@here")
|
---|
| 30 | >>> s.getreply()
|
---|
| 31 | (250, "Somebody OverHere <somebody@here.my.org>")
|
---|
| 32 | >>> s.quit()
|
---|
| 33 | '''
|
---|
| 34 |
|
---|
| 35 | # Author: The Dragon De Monsyne <dragondm@integral.org>
|
---|
| 36 | # ESMTP support, test code and doc fixes added by
|
---|
| 37 | # Eric S. Raymond <esr@thyrsus.com>
|
---|
| 38 | # Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
|
---|
| 39 | # by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
|
---|
| 40 | # RFC 2554 (authentication) support by Gerhard Haering <gerhard@bigfoot.de>.
|
---|
| 41 | #
|
---|
| 42 | # This was modified from the Python 1.5 library HTTP lib.
|
---|
| 43 |
|
---|
| 44 | import socket
|
---|
| 45 | import re
|
---|
| 46 | import email.utils
|
---|
| 47 | import base64
|
---|
| 48 | import hmac
|
---|
| 49 | from email.base64mime import encode as encode_base64
|
---|
| 50 | from sys import stderr
|
---|
| 51 |
|
---|
[391] | 52 | __all__ = ["SMTPException", "SMTPServerDisconnected", "SMTPResponseException",
|
---|
| 53 | "SMTPSenderRefused", "SMTPRecipientsRefused", "SMTPDataError",
|
---|
| 54 | "SMTPConnectError", "SMTPHeloError", "SMTPAuthenticationError",
|
---|
| 55 | "quoteaddr", "quotedata", "SMTP"]
|
---|
[2] | 56 |
|
---|
| 57 | SMTP_PORT = 25
|
---|
| 58 | SMTP_SSL_PORT = 465
|
---|
[391] | 59 | CRLF = "\r\n"
|
---|
[2] | 60 |
|
---|
| 61 | OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
|
---|
| 62 |
|
---|
[391] | 63 |
|
---|
[2] | 64 | # Exception classes used by this module.
|
---|
| 65 | class SMTPException(Exception):
|
---|
| 66 | """Base class for all exceptions raised by this module."""
|
---|
| 67 |
|
---|
| 68 | class SMTPServerDisconnected(SMTPException):
|
---|
| 69 | """Not connected to any SMTP server.
|
---|
| 70 |
|
---|
| 71 | This exception is raised when the server unexpectedly disconnects,
|
---|
| 72 | or when an attempt is made to use the SMTP instance before
|
---|
| 73 | connecting it to a server.
|
---|
| 74 | """
|
---|
| 75 |
|
---|
| 76 | class SMTPResponseException(SMTPException):
|
---|
| 77 | """Base class for all exceptions that include an SMTP error code.
|
---|
| 78 |
|
---|
| 79 | These exceptions are generated in some instances when the SMTP
|
---|
| 80 | server returns an error code. The error code is stored in the
|
---|
| 81 | `smtp_code' attribute of the error, and the `smtp_error' attribute
|
---|
| 82 | is set to the error message.
|
---|
| 83 | """
|
---|
| 84 |
|
---|
| 85 | def __init__(self, code, msg):
|
---|
| 86 | self.smtp_code = code
|
---|
| 87 | self.smtp_error = msg
|
---|
| 88 | self.args = (code, msg)
|
---|
| 89 |
|
---|
| 90 | class SMTPSenderRefused(SMTPResponseException):
|
---|
| 91 | """Sender address refused.
|
---|
| 92 |
|
---|
| 93 | In addition to the attributes set by on all SMTPResponseException
|
---|
| 94 | exceptions, this sets `sender' to the string that the SMTP refused.
|
---|
| 95 | """
|
---|
| 96 |
|
---|
| 97 | def __init__(self, code, msg, sender):
|
---|
| 98 | self.smtp_code = code
|
---|
| 99 | self.smtp_error = msg
|
---|
| 100 | self.sender = sender
|
---|
| 101 | self.args = (code, msg, sender)
|
---|
| 102 |
|
---|
| 103 | class SMTPRecipientsRefused(SMTPException):
|
---|
| 104 | """All recipient addresses refused.
|
---|
| 105 |
|
---|
| 106 | The errors for each recipient are accessible through the attribute
|
---|
| 107 | 'recipients', which is a dictionary of exactly the same sort as
|
---|
| 108 | SMTP.sendmail() returns.
|
---|
| 109 | """
|
---|
| 110 |
|
---|
| 111 | def __init__(self, recipients):
|
---|
| 112 | self.recipients = recipients
|
---|
[391] | 113 | self.args = (recipients,)
|
---|
[2] | 114 |
|
---|
| 115 |
|
---|
| 116 | class SMTPDataError(SMTPResponseException):
|
---|
| 117 | """The SMTP server didn't accept the data."""
|
---|
| 118 |
|
---|
| 119 | class SMTPConnectError(SMTPResponseException):
|
---|
| 120 | """Error during connection establishment."""
|
---|
| 121 |
|
---|
| 122 | class SMTPHeloError(SMTPResponseException):
|
---|
| 123 | """The server refused our HELO reply."""
|
---|
| 124 |
|
---|
| 125 | class SMTPAuthenticationError(SMTPResponseException):
|
---|
| 126 | """Authentication error.
|
---|
| 127 |
|
---|
| 128 | Most probably the server didn't accept the username/password
|
---|
| 129 | combination provided.
|
---|
| 130 | """
|
---|
| 131 |
|
---|
[391] | 132 |
|
---|
[2] | 133 | def quoteaddr(addr):
|
---|
| 134 | """Quote a subset of the email addresses defined by RFC 821.
|
---|
| 135 |
|
---|
| 136 | Should be able to handle anything rfc822.parseaddr can handle.
|
---|
| 137 | """
|
---|
| 138 | m = (None, None)
|
---|
| 139 | try:
|
---|
| 140 | m = email.utils.parseaddr(addr)[1]
|
---|
| 141 | except AttributeError:
|
---|
| 142 | pass
|
---|
[391] | 143 | if m == (None, None): # Indicates parse failure or AttributeError
|
---|
[2] | 144 | # something weird here.. punt -ddm
|
---|
| 145 | return "<%s>" % addr
|
---|
| 146 | elif m is None:
|
---|
| 147 | # the sender wants an empty return address
|
---|
| 148 | return "<>"
|
---|
| 149 | else:
|
---|
| 150 | return "<%s>" % m
|
---|
| 151 |
|
---|
[391] | 152 | def _addr_only(addrstring):
|
---|
| 153 | displayname, addr = email.utils.parseaddr(addrstring)
|
---|
| 154 | if (displayname, addr) == ('', ''):
|
---|
| 155 | # parseaddr couldn't parse it, so use it as is.
|
---|
| 156 | return addrstring
|
---|
| 157 | return addr
|
---|
| 158 |
|
---|
[2] | 159 | def quotedata(data):
|
---|
| 160 | """Quote data for email.
|
---|
| 161 |
|
---|
| 162 | Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
|
---|
| 163 | Internet CRLF end-of-line.
|
---|
| 164 | """
|
---|
| 165 | return re.sub(r'(?m)^\.', '..',
|
---|
| 166 | re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
|
---|
| 167 |
|
---|
| 168 |
|
---|
| 169 | try:
|
---|
| 170 | import ssl
|
---|
| 171 | except ImportError:
|
---|
| 172 | _have_ssl = False
|
---|
| 173 | else:
|
---|
| 174 | class SSLFakeFile:
|
---|
| 175 | """A fake file like object that really wraps a SSLObject.
|
---|
| 176 |
|
---|
| 177 | It only supports what is needed in smtplib.
|
---|
| 178 | """
|
---|
| 179 | def __init__(self, sslobj):
|
---|
| 180 | self.sslobj = sslobj
|
---|
| 181 |
|
---|
| 182 | def readline(self):
|
---|
| 183 | str = ""
|
---|
| 184 | chr = None
|
---|
| 185 | while chr != "\n":
|
---|
| 186 | chr = self.sslobj.read(1)
|
---|
[391] | 187 | if not chr:
|
---|
| 188 | break
|
---|
[2] | 189 | str += chr
|
---|
| 190 | return str
|
---|
| 191 |
|
---|
| 192 | def close(self):
|
---|
| 193 | pass
|
---|
| 194 |
|
---|
| 195 | _have_ssl = True
|
---|
| 196 |
|
---|
| 197 | class SMTP:
|
---|
| 198 | """This class manages a connection to an SMTP or ESMTP server.
|
---|
| 199 | SMTP Objects:
|
---|
| 200 | SMTP objects have the following attributes:
|
---|
| 201 | helo_resp
|
---|
| 202 | This is the message given by the server in response to the
|
---|
| 203 | most recent HELO command.
|
---|
| 204 |
|
---|
| 205 | ehlo_resp
|
---|
| 206 | This is the message given by the server in response to the
|
---|
| 207 | most recent EHLO command. This is usually multiline.
|
---|
| 208 |
|
---|
| 209 | does_esmtp
|
---|
| 210 | This is a True value _after you do an EHLO command_, if the
|
---|
| 211 | server supports ESMTP.
|
---|
| 212 |
|
---|
| 213 | esmtp_features
|
---|
| 214 | This is a dictionary, which, if the server supports ESMTP,
|
---|
| 215 | will _after you do an EHLO command_, contain the names of the
|
---|
| 216 | SMTP service extensions this server supports, and their
|
---|
| 217 | parameters (if any).
|
---|
| 218 |
|
---|
| 219 | Note, all extension names are mapped to lower case in the
|
---|
| 220 | dictionary.
|
---|
| 221 |
|
---|
| 222 | See each method's docstrings for details. In general, there is a
|
---|
| 223 | method of the same name to perform each SMTP command. There is also a
|
---|
| 224 | method called 'sendmail' that will do an entire mail transaction.
|
---|
| 225 | """
|
---|
| 226 | debuglevel = 0
|
---|
| 227 | file = None
|
---|
| 228 | helo_resp = None
|
---|
| 229 | ehlo_msg = "ehlo"
|
---|
| 230 | ehlo_resp = None
|
---|
| 231 | does_esmtp = 0
|
---|
[391] | 232 | default_port = SMTP_PORT
|
---|
[2] | 233 |
|
---|
| 234 | def __init__(self, host='', port=0, local_hostname=None,
|
---|
| 235 | timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
|
---|
| 236 | """Initialize a new instance.
|
---|
| 237 |
|
---|
| 238 | If specified, `host' is the name of the remote host to which to
|
---|
| 239 | connect. If specified, `port' specifies the port to which to connect.
|
---|
[391] | 240 | By default, smtplib.SMTP_PORT is used. If a host is specified the
|
---|
| 241 | connect method is called, and if it returns anything other than a
|
---|
| 242 | success code an SMTPConnectError is raised. If specified,
|
---|
| 243 | `local_hostname` is used as the FQDN of the local host for the
|
---|
| 244 | HELO/EHLO command. Otherwise, the local hostname is found using
|
---|
| 245 | socket.getfqdn().
|
---|
[2] | 246 |
|
---|
| 247 | """
|
---|
| 248 | self.timeout = timeout
|
---|
| 249 | self.esmtp_features = {}
|
---|
| 250 | if host:
|
---|
| 251 | (code, msg) = self.connect(host, port)
|
---|
| 252 | if code != 220:
|
---|
| 253 | raise SMTPConnectError(code, msg)
|
---|
| 254 | if local_hostname is not None:
|
---|
| 255 | self.local_hostname = local_hostname
|
---|
| 256 | else:
|
---|
| 257 | # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
|
---|
| 258 | # if that can't be calculated, that we should use a domain literal
|
---|
| 259 | # instead (essentially an encoded IP address like [A.B.C.D]).
|
---|
| 260 | fqdn = socket.getfqdn()
|
---|
| 261 | if '.' in fqdn:
|
---|
| 262 | self.local_hostname = fqdn
|
---|
| 263 | else:
|
---|
| 264 | # We can't find an fqdn hostname, so use a domain literal
|
---|
| 265 | addr = '127.0.0.1'
|
---|
| 266 | try:
|
---|
| 267 | addr = socket.gethostbyname(socket.gethostname())
|
---|
| 268 | except socket.gaierror:
|
---|
| 269 | pass
|
---|
| 270 | self.local_hostname = '[%s]' % addr
|
---|
| 271 |
|
---|
| 272 | def set_debuglevel(self, debuglevel):
|
---|
| 273 | """Set the debug output level.
|
---|
| 274 |
|
---|
| 275 | A non-false value results in debug messages for connection and for all
|
---|
| 276 | messages sent to and received from the server.
|
---|
| 277 |
|
---|
| 278 | """
|
---|
| 279 | self.debuglevel = debuglevel
|
---|
| 280 |
|
---|
[391] | 281 | def _get_socket(self, host, port, timeout):
|
---|
[2] | 282 | # This makes it simpler for SMTP_SSL to use the SMTP connect code
|
---|
| 283 | # and just alter the socket connection bit.
|
---|
[391] | 284 | if self.debuglevel > 0:
|
---|
| 285 | print>>stderr, 'connect:', (host, port)
|
---|
| 286 | return socket.create_connection((host, port), timeout)
|
---|
[2] | 287 |
|
---|
[391] | 288 | def connect(self, host='localhost', port=0):
|
---|
[2] | 289 | """Connect to a host on a given port.
|
---|
| 290 |
|
---|
| 291 | If the hostname ends with a colon (`:') followed by a number, and
|
---|
| 292 | there is no port specified, that suffix will be stripped off and the
|
---|
| 293 | number interpreted as the port number to use.
|
---|
| 294 |
|
---|
| 295 | Note: This method is automatically invoked by __init__, if a host is
|
---|
| 296 | specified during instantiation.
|
---|
| 297 |
|
---|
| 298 | """
|
---|
| 299 | if not port and (host.find(':') == host.rfind(':')):
|
---|
| 300 | i = host.rfind(':')
|
---|
| 301 | if i >= 0:
|
---|
[391] | 302 | host, port = host[:i], host[i + 1:]
|
---|
| 303 | try:
|
---|
| 304 | port = int(port)
|
---|
[2] | 305 | except ValueError:
|
---|
| 306 | raise socket.error, "nonnumeric port"
|
---|
[391] | 307 | if not port:
|
---|
| 308 | port = self.default_port
|
---|
| 309 | if self.debuglevel > 0:
|
---|
| 310 | print>>stderr, 'connect:', (host, port)
|
---|
[2] | 311 | self.sock = self._get_socket(host, port, self.timeout)
|
---|
| 312 | (code, msg) = self.getreply()
|
---|
[391] | 313 | if self.debuglevel > 0:
|
---|
| 314 | print>>stderr, "connect:", msg
|
---|
[2] | 315 | return (code, msg)
|
---|
| 316 |
|
---|
| 317 | def send(self, str):
|
---|
| 318 | """Send `str' to the server."""
|
---|
[391] | 319 | if self.debuglevel > 0:
|
---|
| 320 | print>>stderr, 'send:', repr(str)
|
---|
[2] | 321 | if hasattr(self, 'sock') and self.sock:
|
---|
| 322 | try:
|
---|
| 323 | self.sock.sendall(str)
|
---|
| 324 | except socket.error:
|
---|
| 325 | self.close()
|
---|
| 326 | raise SMTPServerDisconnected('Server not connected')
|
---|
| 327 | else:
|
---|
| 328 | raise SMTPServerDisconnected('please run connect() first')
|
---|
| 329 |
|
---|
| 330 | def putcmd(self, cmd, args=""):
|
---|
| 331 | """Send a command to the server."""
|
---|
| 332 | if args == "":
|
---|
| 333 | str = '%s%s' % (cmd, CRLF)
|
---|
| 334 | else:
|
---|
| 335 | str = '%s %s%s' % (cmd, args, CRLF)
|
---|
| 336 | self.send(str)
|
---|
| 337 |
|
---|
| 338 | def getreply(self):
|
---|
| 339 | """Get a reply from the server.
|
---|
| 340 |
|
---|
| 341 | Returns a tuple consisting of:
|
---|
| 342 |
|
---|
| 343 | - server response code (e.g. '250', or such, if all goes well)
|
---|
| 344 | Note: returns -1 if it can't read response code.
|
---|
| 345 |
|
---|
| 346 | - server response string corresponding to response code (multiline
|
---|
| 347 | responses are converted to a single, multiline string).
|
---|
| 348 |
|
---|
| 349 | Raises SMTPServerDisconnected if end-of-file is reached.
|
---|
| 350 | """
|
---|
[391] | 351 | resp = []
|
---|
[2] | 352 | if self.file is None:
|
---|
| 353 | self.file = self.sock.makefile('rb')
|
---|
| 354 | while 1:
|
---|
[391] | 355 | try:
|
---|
| 356 | line = self.file.readline()
|
---|
| 357 | except socket.error as e:
|
---|
| 358 | self.close()
|
---|
| 359 | raise SMTPServerDisconnected("Connection unexpectedly closed: "
|
---|
| 360 | + str(e))
|
---|
[2] | 361 | if line == '':
|
---|
| 362 | self.close()
|
---|
| 363 | raise SMTPServerDisconnected("Connection unexpectedly closed")
|
---|
[391] | 364 | if self.debuglevel > 0:
|
---|
| 365 | print>>stderr, 'reply:', repr(line)
|
---|
[2] | 366 | resp.append(line[4:].strip())
|
---|
[391] | 367 | code = line[:3]
|
---|
[2] | 368 | # Check that the error code is syntactically correct.
|
---|
| 369 | # Don't attempt to read a continuation line if it is broken.
|
---|
| 370 | try:
|
---|
| 371 | errcode = int(code)
|
---|
| 372 | except ValueError:
|
---|
| 373 | errcode = -1
|
---|
| 374 | break
|
---|
| 375 | # Check if multiline response.
|
---|
[391] | 376 | if line[3:4] != "-":
|
---|
[2] | 377 | break
|
---|
| 378 |
|
---|
| 379 | errmsg = "\n".join(resp)
|
---|
| 380 | if self.debuglevel > 0:
|
---|
[391] | 381 | print>>stderr, 'reply: retcode (%s); Msg: %s' % (errcode, errmsg)
|
---|
[2] | 382 | return errcode, errmsg
|
---|
| 383 |
|
---|
| 384 | def docmd(self, cmd, args=""):
|
---|
| 385 | """Send a command, and return its response code."""
|
---|
[391] | 386 | self.putcmd(cmd, args)
|
---|
[2] | 387 | return self.getreply()
|
---|
| 388 |
|
---|
| 389 | # std smtp commands
|
---|
| 390 | def helo(self, name=''):
|
---|
| 391 | """SMTP 'helo' command.
|
---|
| 392 | Hostname to send for this command defaults to the FQDN of the local
|
---|
| 393 | host.
|
---|
| 394 | """
|
---|
| 395 | self.putcmd("helo", name or self.local_hostname)
|
---|
[391] | 396 | (code, msg) = self.getreply()
|
---|
| 397 | self.helo_resp = msg
|
---|
| 398 | return (code, msg)
|
---|
[2] | 399 |
|
---|
| 400 | def ehlo(self, name=''):
|
---|
| 401 | """ SMTP 'ehlo' command.
|
---|
| 402 | Hostname to send for this command defaults to the FQDN of the local
|
---|
| 403 | host.
|
---|
| 404 | """
|
---|
| 405 | self.esmtp_features = {}
|
---|
| 406 | self.putcmd(self.ehlo_msg, name or self.local_hostname)
|
---|
[391] | 407 | (code, msg) = self.getreply()
|
---|
[2] | 408 | # According to RFC1869 some (badly written)
|
---|
| 409 | # MTA's will disconnect on an ehlo. Toss an exception if
|
---|
| 410 | # that happens -ddm
|
---|
| 411 | if code == -1 and len(msg) == 0:
|
---|
| 412 | self.close()
|
---|
| 413 | raise SMTPServerDisconnected("Server not connected")
|
---|
[391] | 414 | self.ehlo_resp = msg
|
---|
[2] | 415 | if code != 250:
|
---|
[391] | 416 | return (code, msg)
|
---|
| 417 | self.does_esmtp = 1
|
---|
[2] | 418 | #parse the ehlo response -ddm
|
---|
[391] | 419 | resp = self.ehlo_resp.split('\n')
|
---|
[2] | 420 | del resp[0]
|
---|
| 421 | for each in resp:
|
---|
| 422 | # To be able to communicate with as many SMTP servers as possible,
|
---|
| 423 | # we have to take the old-style auth advertisement into account,
|
---|
| 424 | # because:
|
---|
| 425 | # 1) Else our SMTP feature parser gets confused.
|
---|
| 426 | # 2) There are some servers that only advertise the auth methods we
|
---|
| 427 | # support using the old style.
|
---|
| 428 | auth_match = OLDSTYLE_AUTH.match(each)
|
---|
| 429 | if auth_match:
|
---|
| 430 | # This doesn't remove duplicates, but that's no problem
|
---|
| 431 | self.esmtp_features["auth"] = self.esmtp_features.get("auth", "") \
|
---|
| 432 | + " " + auth_match.groups(0)[0]
|
---|
| 433 | continue
|
---|
| 434 |
|
---|
| 435 | # RFC 1869 requires a space between ehlo keyword and parameters.
|
---|
| 436 | # It's actually stricter, in that only spaces are allowed between
|
---|
| 437 | # parameters, but were not going to check for that here. Note
|
---|
| 438 | # that the space isn't present if there are no parameters.
|
---|
[391] | 439 | m = re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?', each)
|
---|
[2] | 440 | if m:
|
---|
[391] | 441 | feature = m.group("feature").lower()
|
---|
| 442 | params = m.string[m.end("feature"):].strip()
|
---|
[2] | 443 | if feature == "auth":
|
---|
| 444 | self.esmtp_features[feature] = self.esmtp_features.get(feature, "") \
|
---|
| 445 | + " " + params
|
---|
| 446 | else:
|
---|
[391] | 447 | self.esmtp_features[feature] = params
|
---|
| 448 | return (code, msg)
|
---|
[2] | 449 |
|
---|
| 450 | def has_extn(self, opt):
|
---|
| 451 | """Does the server support a given SMTP service extension?"""
|
---|
| 452 | return opt.lower() in self.esmtp_features
|
---|
| 453 |
|
---|
| 454 | def help(self, args=''):
|
---|
| 455 | """SMTP 'help' command.
|
---|
| 456 | Returns help text from server."""
|
---|
| 457 | self.putcmd("help", args)
|
---|
| 458 | return self.getreply()[1]
|
---|
| 459 |
|
---|
| 460 | def rset(self):
|
---|
| 461 | """SMTP 'rset' command -- resets session."""
|
---|
| 462 | return self.docmd("rset")
|
---|
| 463 |
|
---|
| 464 | def noop(self):
|
---|
| 465 | """SMTP 'noop' command -- doesn't do anything :>"""
|
---|
| 466 | return self.docmd("noop")
|
---|
| 467 |
|
---|
[391] | 468 | def mail(self, sender, options=[]):
|
---|
[2] | 469 | """SMTP 'mail' command -- begins mail xfer session."""
|
---|
| 470 | optionlist = ''
|
---|
| 471 | if options and self.does_esmtp:
|
---|
| 472 | optionlist = ' ' + ' '.join(options)
|
---|
[391] | 473 | self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender), optionlist))
|
---|
[2] | 474 | return self.getreply()
|
---|
| 475 |
|
---|
[391] | 476 | def rcpt(self, recip, options=[]):
|
---|
[2] | 477 | """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
|
---|
| 478 | optionlist = ''
|
---|
| 479 | if options and self.does_esmtp:
|
---|
| 480 | optionlist = ' ' + ' '.join(options)
|
---|
[391] | 481 | self.putcmd("rcpt", "TO:%s%s" % (quoteaddr(recip), optionlist))
|
---|
[2] | 482 | return self.getreply()
|
---|
| 483 |
|
---|
[391] | 484 | def data(self, msg):
|
---|
[2] | 485 | """SMTP 'DATA' command -- sends message data to server.
|
---|
| 486 |
|
---|
| 487 | Automatically quotes lines beginning with a period per rfc821.
|
---|
| 488 | Raises SMTPDataError if there is an unexpected reply to the
|
---|
| 489 | DATA command; the return value from this method is the final
|
---|
| 490 | response code received when the all data is sent.
|
---|
| 491 | """
|
---|
| 492 | self.putcmd("data")
|
---|
[391] | 493 | (code, repl) = self.getreply()
|
---|
| 494 | if self.debuglevel > 0:
|
---|
| 495 | print>>stderr, "data:", (code, repl)
|
---|
[2] | 496 | if code != 354:
|
---|
[391] | 497 | raise SMTPDataError(code, repl)
|
---|
[2] | 498 | else:
|
---|
| 499 | q = quotedata(msg)
|
---|
| 500 | if q[-2:] != CRLF:
|
---|
| 501 | q = q + CRLF
|
---|
| 502 | q = q + "." + CRLF
|
---|
| 503 | self.send(q)
|
---|
[391] | 504 | (code, msg) = self.getreply()
|
---|
| 505 | if self.debuglevel > 0:
|
---|
| 506 | print>>stderr, "data:", (code, msg)
|
---|
| 507 | return (code, msg)
|
---|
[2] | 508 |
|
---|
| 509 | def verify(self, address):
|
---|
| 510 | """SMTP 'verify' command -- checks for address validity."""
|
---|
[391] | 511 | self.putcmd("vrfy", _addr_only(address))
|
---|
[2] | 512 | return self.getreply()
|
---|
| 513 | # a.k.a.
|
---|
[391] | 514 | vrfy = verify
|
---|
[2] | 515 |
|
---|
| 516 | def expn(self, address):
|
---|
| 517 | """SMTP 'expn' command -- expands a mailing list."""
|
---|
[391] | 518 | self.putcmd("expn", _addr_only(address))
|
---|
[2] | 519 | return self.getreply()
|
---|
| 520 |
|
---|
| 521 | # some useful methods
|
---|
| 522 |
|
---|
| 523 | def ehlo_or_helo_if_needed(self):
|
---|
| 524 | """Call self.ehlo() and/or self.helo() if needed.
|
---|
| 525 |
|
---|
| 526 | If there has been no previous EHLO or HELO command this session, this
|
---|
| 527 | method tries ESMTP EHLO first.
|
---|
| 528 |
|
---|
| 529 | This method may raise the following exceptions:
|
---|
| 530 |
|
---|
| 531 | SMTPHeloError The server didn't reply properly to
|
---|
| 532 | the helo greeting.
|
---|
| 533 | """
|
---|
| 534 | if self.helo_resp is None and self.ehlo_resp is None:
|
---|
| 535 | if not (200 <= self.ehlo()[0] <= 299):
|
---|
| 536 | (code, resp) = self.helo()
|
---|
| 537 | if not (200 <= code <= 299):
|
---|
| 538 | raise SMTPHeloError(code, resp)
|
---|
| 539 |
|
---|
| 540 | def login(self, user, password):
|
---|
| 541 | """Log in on an SMTP server that requires authentication.
|
---|
| 542 |
|
---|
| 543 | The arguments are:
|
---|
| 544 | - user: The user name to authenticate with.
|
---|
| 545 | - password: The password for the authentication.
|
---|
| 546 |
|
---|
| 547 | If there has been no previous EHLO or HELO command this session, this
|
---|
| 548 | method tries ESMTP EHLO first.
|
---|
| 549 |
|
---|
| 550 | This method will return normally if the authentication was successful.
|
---|
| 551 |
|
---|
| 552 | This method may raise the following exceptions:
|
---|
| 553 |
|
---|
| 554 | SMTPHeloError The server didn't reply properly to
|
---|
| 555 | the helo greeting.
|
---|
| 556 | SMTPAuthenticationError The server didn't accept the username/
|
---|
| 557 | password combination.
|
---|
| 558 | SMTPException No suitable authentication method was
|
---|
| 559 | found.
|
---|
| 560 | """
|
---|
| 561 |
|
---|
| 562 | def encode_cram_md5(challenge, user, password):
|
---|
| 563 | challenge = base64.decodestring(challenge)
|
---|
| 564 | response = user + " " + hmac.HMAC(password, challenge).hexdigest()
|
---|
| 565 | return encode_base64(response, eol="")
|
---|
| 566 |
|
---|
| 567 | def encode_plain(user, password):
|
---|
| 568 | return encode_base64("\0%s\0%s" % (user, password), eol="")
|
---|
| 569 |
|
---|
| 570 |
|
---|
| 571 | AUTH_PLAIN = "PLAIN"
|
---|
| 572 | AUTH_CRAM_MD5 = "CRAM-MD5"
|
---|
| 573 | AUTH_LOGIN = "LOGIN"
|
---|
| 574 |
|
---|
| 575 | self.ehlo_or_helo_if_needed()
|
---|
| 576 |
|
---|
| 577 | if not self.has_extn("auth"):
|
---|
| 578 | raise SMTPException("SMTP AUTH extension not supported by server.")
|
---|
| 579 |
|
---|
| 580 | # Authentication methods the server supports:
|
---|
| 581 | authlist = self.esmtp_features["auth"].split()
|
---|
| 582 |
|
---|
| 583 | # List of authentication methods we support: from preferred to
|
---|
| 584 | # less preferred methods. Except for the purpose of testing the weaker
|
---|
| 585 | # ones, we prefer stronger methods like CRAM-MD5:
|
---|
| 586 | preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN, AUTH_LOGIN]
|
---|
| 587 |
|
---|
| 588 | # Determine the authentication method we'll use
|
---|
| 589 | authmethod = None
|
---|
| 590 | for method in preferred_auths:
|
---|
| 591 | if method in authlist:
|
---|
| 592 | authmethod = method
|
---|
| 593 | break
|
---|
| 594 |
|
---|
| 595 | if authmethod == AUTH_CRAM_MD5:
|
---|
| 596 | (code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
|
---|
| 597 | if code == 503:
|
---|
| 598 | # 503 == 'Error: already authenticated'
|
---|
| 599 | return (code, resp)
|
---|
| 600 | (code, resp) = self.docmd(encode_cram_md5(resp, user, password))
|
---|
| 601 | elif authmethod == AUTH_PLAIN:
|
---|
| 602 | (code, resp) = self.docmd("AUTH",
|
---|
| 603 | AUTH_PLAIN + " " + encode_plain(user, password))
|
---|
| 604 | elif authmethod == AUTH_LOGIN:
|
---|
| 605 | (code, resp) = self.docmd("AUTH",
|
---|
| 606 | "%s %s" % (AUTH_LOGIN, encode_base64(user, eol="")))
|
---|
| 607 | if code != 334:
|
---|
| 608 | raise SMTPAuthenticationError(code, resp)
|
---|
| 609 | (code, resp) = self.docmd(encode_base64(password, eol=""))
|
---|
| 610 | elif authmethod is None:
|
---|
| 611 | raise SMTPException("No suitable authentication method found.")
|
---|
| 612 | if code not in (235, 503):
|
---|
| 613 | # 235 == 'Authentication successful'
|
---|
| 614 | # 503 == 'Error: already authenticated'
|
---|
| 615 | raise SMTPAuthenticationError(code, resp)
|
---|
| 616 | return (code, resp)
|
---|
| 617 |
|
---|
[391] | 618 | def starttls(self, keyfile=None, certfile=None):
|
---|
[2] | 619 | """Puts the connection to the SMTP server into TLS mode.
|
---|
| 620 |
|
---|
| 621 | If there has been no previous EHLO or HELO command this session, this
|
---|
| 622 | method tries ESMTP EHLO first.
|
---|
| 623 |
|
---|
| 624 | If the server supports TLS, this will encrypt the rest of the SMTP
|
---|
| 625 | session. If you provide the keyfile and certfile parameters,
|
---|
| 626 | the identity of the SMTP server and client can be checked. This,
|
---|
| 627 | however, depends on whether the socket module really checks the
|
---|
| 628 | certificates.
|
---|
| 629 |
|
---|
| 630 | This method may raise the following exceptions:
|
---|
| 631 |
|
---|
| 632 | SMTPHeloError The server didn't reply properly to
|
---|
| 633 | the helo greeting.
|
---|
| 634 | """
|
---|
| 635 | self.ehlo_or_helo_if_needed()
|
---|
| 636 | if not self.has_extn("starttls"):
|
---|
| 637 | raise SMTPException("STARTTLS extension not supported by server.")
|
---|
| 638 | (resp, reply) = self.docmd("STARTTLS")
|
---|
| 639 | if resp == 220:
|
---|
| 640 | if not _have_ssl:
|
---|
| 641 | raise RuntimeError("No SSL support included in this Python")
|
---|
| 642 | self.sock = ssl.wrap_socket(self.sock, keyfile, certfile)
|
---|
| 643 | self.file = SSLFakeFile(self.sock)
|
---|
| 644 | # RFC 3207:
|
---|
| 645 | # The client MUST discard any knowledge obtained from
|
---|
| 646 | # the server, such as the list of SMTP service extensions,
|
---|
| 647 | # which was not obtained from the TLS negotiation itself.
|
---|
| 648 | self.helo_resp = None
|
---|
| 649 | self.ehlo_resp = None
|
---|
| 650 | self.esmtp_features = {}
|
---|
| 651 | self.does_esmtp = 0
|
---|
| 652 | return (resp, reply)
|
---|
| 653 |
|
---|
| 654 | def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
|
---|
| 655 | rcpt_options=[]):
|
---|
| 656 | """This command performs an entire mail transaction.
|
---|
| 657 |
|
---|
| 658 | The arguments are:
|
---|
| 659 | - from_addr : The address sending this mail.
|
---|
| 660 | - to_addrs : A list of addresses to send this mail to. A bare
|
---|
| 661 | string will be treated as a list with 1 address.
|
---|
| 662 | - msg : The message to send.
|
---|
| 663 | - mail_options : List of ESMTP options (such as 8bitmime) for the
|
---|
| 664 | mail command.
|
---|
| 665 | - rcpt_options : List of ESMTP options (such as DSN commands) for
|
---|
| 666 | all the rcpt commands.
|
---|
| 667 |
|
---|
| 668 | If there has been no previous EHLO or HELO command this session, this
|
---|
| 669 | method tries ESMTP EHLO first. If the server does ESMTP, message size
|
---|
| 670 | and each of the specified options will be passed to it. If EHLO
|
---|
| 671 | fails, HELO will be tried and ESMTP options suppressed.
|
---|
| 672 |
|
---|
| 673 | This method will return normally if the mail is accepted for at least
|
---|
| 674 | one recipient. It returns a dictionary, with one entry for each
|
---|
| 675 | recipient that was refused. Each entry contains a tuple of the SMTP
|
---|
| 676 | error code and the accompanying error message sent by the server.
|
---|
| 677 |
|
---|
| 678 | This method may raise the following exceptions:
|
---|
| 679 |
|
---|
| 680 | SMTPHeloError The server didn't reply properly to
|
---|
| 681 | the helo greeting.
|
---|
| 682 | SMTPRecipientsRefused The server rejected ALL recipients
|
---|
| 683 | (no mail was sent).
|
---|
| 684 | SMTPSenderRefused The server didn't accept the from_addr.
|
---|
| 685 | SMTPDataError The server replied with an unexpected
|
---|
| 686 | error code (other than a refusal of
|
---|
| 687 | a recipient).
|
---|
| 688 |
|
---|
| 689 | Note: the connection will be open even after an exception is raised.
|
---|
| 690 |
|
---|
| 691 | Example:
|
---|
| 692 |
|
---|
| 693 | >>> import smtplib
|
---|
| 694 | >>> s=smtplib.SMTP("localhost")
|
---|
| 695 | >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
|
---|
| 696 | >>> msg = '''\\
|
---|
| 697 | ... From: Me@my.org
|
---|
| 698 | ... Subject: testin'...
|
---|
| 699 | ...
|
---|
| 700 | ... This is a test '''
|
---|
| 701 | >>> s.sendmail("me@my.org",tolist,msg)
|
---|
| 702 | { "three@three.org" : ( 550 ,"User unknown" ) }
|
---|
| 703 | >>> s.quit()
|
---|
| 704 |
|
---|
| 705 | In the above example, the message was accepted for delivery to three
|
---|
| 706 | of the four addresses, and one was rejected, with the error code
|
---|
| 707 | 550. If all addresses are accepted, then the method will return an
|
---|
| 708 | empty dictionary.
|
---|
| 709 |
|
---|
| 710 | """
|
---|
| 711 | self.ehlo_or_helo_if_needed()
|
---|
| 712 | esmtp_opts = []
|
---|
| 713 | if self.does_esmtp:
|
---|
| 714 | # Hmmm? what's this? -ddm
|
---|
| 715 | # self.esmtp_features['7bit']=""
|
---|
| 716 | if self.has_extn('size'):
|
---|
| 717 | esmtp_opts.append("size=%d" % len(msg))
|
---|
| 718 | for option in mail_options:
|
---|
| 719 | esmtp_opts.append(option)
|
---|
| 720 |
|
---|
[391] | 721 | (code, resp) = self.mail(from_addr, esmtp_opts)
|
---|
[2] | 722 | if code != 250:
|
---|
| 723 | self.rset()
|
---|
| 724 | raise SMTPSenderRefused(code, resp, from_addr)
|
---|
[391] | 725 | senderrs = {}
|
---|
[2] | 726 | if isinstance(to_addrs, basestring):
|
---|
| 727 | to_addrs = [to_addrs]
|
---|
| 728 | for each in to_addrs:
|
---|
[391] | 729 | (code, resp) = self.rcpt(each, rcpt_options)
|
---|
[2] | 730 | if (code != 250) and (code != 251):
|
---|
[391] | 731 | senderrs[each] = (code, resp)
|
---|
| 732 | if len(senderrs) == len(to_addrs):
|
---|
[2] | 733 | # the server refused all our recipients
|
---|
| 734 | self.rset()
|
---|
| 735 | raise SMTPRecipientsRefused(senderrs)
|
---|
[391] | 736 | (code, resp) = self.data(msg)
|
---|
[2] | 737 | if code != 250:
|
---|
| 738 | self.rset()
|
---|
| 739 | raise SMTPDataError(code, resp)
|
---|
| 740 | #if we got here then somebody got our mail
|
---|
| 741 | return senderrs
|
---|
| 742 |
|
---|
| 743 |
|
---|
| 744 | def close(self):
|
---|
| 745 | """Close the connection to the SMTP server."""
|
---|
| 746 | if self.file:
|
---|
| 747 | self.file.close()
|
---|
| 748 | self.file = None
|
---|
| 749 | if self.sock:
|
---|
| 750 | self.sock.close()
|
---|
| 751 | self.sock = None
|
---|
| 752 |
|
---|
| 753 |
|
---|
| 754 | def quit(self):
|
---|
| 755 | """Terminate the SMTP session."""
|
---|
| 756 | res = self.docmd("quit")
|
---|
| 757 | self.close()
|
---|
| 758 | return res
|
---|
| 759 |
|
---|
| 760 | if _have_ssl:
|
---|
| 761 |
|
---|
| 762 | class SMTP_SSL(SMTP):
|
---|
[391] | 763 | """ This is a subclass derived from SMTP that connects over an SSL
|
---|
| 764 | encrypted socket (to use this class you need a socket module that was
|
---|
| 765 | compiled with SSL support). If host is not specified, '' (the local
|
---|
| 766 | host) is used. If port is omitted, the standard SMTP-over-SSL port
|
---|
| 767 | (465) is used. local_hostname has the same meaning as it does in the
|
---|
| 768 | SMTP class. keyfile and certfile are also optional - they can contain
|
---|
| 769 | a PEM formatted private key and certificate chain file for the SSL
|
---|
| 770 | connection.
|
---|
| 771 |
|
---|
[2] | 772 | """
|
---|
[391] | 773 |
|
---|
| 774 | default_port = SMTP_SSL_PORT
|
---|
| 775 |
|
---|
[2] | 776 | def __init__(self, host='', port=0, local_hostname=None,
|
---|
| 777 | keyfile=None, certfile=None,
|
---|
| 778 | timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
|
---|
| 779 | self.keyfile = keyfile
|
---|
| 780 | self.certfile = certfile
|
---|
| 781 | SMTP.__init__(self, host, port, local_hostname, timeout)
|
---|
| 782 |
|
---|
| 783 | def _get_socket(self, host, port, timeout):
|
---|
[391] | 784 | if self.debuglevel > 0:
|
---|
| 785 | print>>stderr, 'connect:', (host, port)
|
---|
[2] | 786 | new_socket = socket.create_connection((host, port), timeout)
|
---|
| 787 | new_socket = ssl.wrap_socket(new_socket, self.keyfile, self.certfile)
|
---|
| 788 | self.file = SSLFakeFile(new_socket)
|
---|
| 789 | return new_socket
|
---|
| 790 |
|
---|
| 791 | __all__.append("SMTP_SSL")
|
---|
| 792 |
|
---|
| 793 | #
|
---|
| 794 | # LMTP extension
|
---|
| 795 | #
|
---|
| 796 | LMTP_PORT = 2003
|
---|
| 797 |
|
---|
| 798 | class LMTP(SMTP):
|
---|
| 799 | """LMTP - Local Mail Transfer Protocol
|
---|
| 800 |
|
---|
| 801 | The LMTP protocol, which is very similar to ESMTP, is heavily based
|
---|
[391] | 802 | on the standard SMTP client. It's common to use Unix sockets for
|
---|
| 803 | LMTP, so our connect() method must support that as well as a regular
|
---|
| 804 | host:port server. local_hostname has the same meaning as it does in
|
---|
| 805 | the SMTP class. To specify a Unix socket, you must use an absolute
|
---|
[2] | 806 | path as the host, starting with a '/'.
|
---|
| 807 |
|
---|
| 808 | Authentication is supported, using the regular SMTP mechanism. When
|
---|
| 809 | using a Unix socket, LMTP generally don't support or require any
|
---|
| 810 | authentication, but your mileage might vary."""
|
---|
| 811 |
|
---|
| 812 | ehlo_msg = "lhlo"
|
---|
| 813 |
|
---|
[391] | 814 | def __init__(self, host='', port=LMTP_PORT, local_hostname=None):
|
---|
[2] | 815 | """Initialize a new instance."""
|
---|
| 816 | SMTP.__init__(self, host, port, local_hostname)
|
---|
| 817 |
|
---|
[391] | 818 | def connect(self, host='localhost', port=0):
|
---|
[2] | 819 | """Connect to the LMTP daemon, on either a Unix or a TCP socket."""
|
---|
| 820 | if host[0] != '/':
|
---|
| 821 | return SMTP.connect(self, host, port)
|
---|
| 822 |
|
---|
| 823 | # Handle Unix-domain sockets.
|
---|
| 824 | try:
|
---|
| 825 | self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
---|
| 826 | self.sock.connect(host)
|
---|
[391] | 827 | except socket.error:
|
---|
| 828 | if self.debuglevel > 0:
|
---|
| 829 | print>>stderr, 'connect fail:', host
|
---|
[2] | 830 | if self.sock:
|
---|
| 831 | self.sock.close()
|
---|
| 832 | self.sock = None
|
---|
[391] | 833 | raise
|
---|
[2] | 834 | (code, msg) = self.getreply()
|
---|
[391] | 835 | if self.debuglevel > 0:
|
---|
| 836 | print>>stderr, "connect:", msg
|
---|
[2] | 837 | return (code, msg)
|
---|
| 838 |
|
---|
| 839 |
|
---|
| 840 | # Test the sendmail method, which tests most of the others.
|
---|
| 841 | # Note: This always sends to localhost.
|
---|
| 842 | if __name__ == '__main__':
|
---|
| 843 | import sys
|
---|
| 844 |
|
---|
| 845 | def prompt(prompt):
|
---|
| 846 | sys.stdout.write(prompt + ": ")
|
---|
| 847 | return sys.stdin.readline().strip()
|
---|
| 848 |
|
---|
| 849 | fromaddr = prompt("From")
|
---|
[391] | 850 | toaddrs = prompt("To").split(',')
|
---|
[2] | 851 | print "Enter message, end with ^D:"
|
---|
| 852 | msg = ''
|
---|
| 853 | while 1:
|
---|
| 854 | line = sys.stdin.readline()
|
---|
| 855 | if not line:
|
---|
| 856 | break
|
---|
| 857 | msg = msg + line
|
---|
| 858 | print "Message length is %d" % len(msg)
|
---|
| 859 |
|
---|
| 860 | server = SMTP('localhost')
|
---|
| 861 | server.set_debuglevel(1)
|
---|
| 862 | server.sendmail(fromaddr, toaddrs, msg)
|
---|
| 863 | server.quit()
|
---|