blob: cf229f3d0db6ae2b8610a598bff74fe999d4576d [file] [log] [blame]
Guido van Rossumbbe323e1998-01-29 17:24:40 +00001"""SMTP Client class.
2
3Author: The Dragon De Monsyne <dragondm@integral.org>
4
5(This was modified from the Python 1.5 library HTTP lib.)
6
7This should follow RFC 821. (it dosen't do esmtp (yet))
8
9Example:
10
11>>> import smtplib
12>>> s=smtplib.SMTP("localhost")
13>>> print s.help()
14This is Sendmail version 8.8.4
15Topics:
16 HELO EHLO MAIL RCPT DATA
17 RSET NOOP QUIT HELP VRFY
18 EXPN VERB ETRN DSN
19For more info use "HELP <topic>".
20To report bugs in the implementation send email to
21 sendmail-bugs@sendmail.org.
22For local information send email to Postmaster at your site.
23End of HELP info
24>>> s.putcmd("vrfy","someone@here")
25>>> s.getreply()
26(250, "Somebody OverHere <somebody@here.my.org>")
27>>> s.quit()
28
29"""
30
31import socket
32import string,re
33
34SMTP_PORT = 25
35CRLF="\r\n"
36
37# used for exceptions
38SMTPSenderRefused="Sender address refused"
39SMTPRecipientsRefused="All Recipients refused"
40SMTPDataError="Error transmoitting message data"
41
42class SMTP:
43 """This class manages a connection to an SMTP server."""
44
45 def __init__(self, host = '', port = 0):
46 """Initialize a new instance.
47
48 If specified, `host' is the name of the remote host to which
49 to connect. If specified, `port' specifies the port to which
50 to connect. By default, smtplib.SMTP_PORT is used.
51
52 """
53 self.debuglevel = 0
54 self.file = None
55 self.helo_resp = None
56 if host: self.connect(host, port)
57
58 def set_debuglevel(self, debuglevel):
59 """Set the debug output level.
60
61 A non-false value results in debug messages for connection and
62 for all messages sent to and received from the server.
63
64 """
65 self.debuglevel = debuglevel
66
67 def connect(self, host='localhost', port = 0):
68 """Connect to a host on a given port.
69
70 Note: This method is automatically invoked by __init__,
71 if a host is specified during instantiation.
72
73 """
74 if not port:
75 i = string.find(host, ':')
76 if i >= 0:
77 host, port = host[:i], host[i+1:]
78 try: port = string.atoi(port)
79 except string.atoi_error:
80 raise socket.error, "nonnumeric port"
81 if not port: port = SMTP_PORT
82 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
83 if self.debuglevel > 0: print 'connect:', (host, port)
84 self.sock.connect(host, port)
85 (code,msg)=self.getreply()
86 if self.debuglevel >0 : print "connect:", msg
87 return msg
88
89 def send(self, str):
90 """Send `str' to the server."""
91 if self.debuglevel > 0: print 'send:', `str`
92 self.sock.send(str)
93
94 def putcmd(self, cmd, args=""):
95 """Send a command to the server.
96 """
97 str = '%s %s%s' % (cmd, args, CRLF)
98 self.send(str)
99
100 def getreply(self):
101 """Get a reply from the server.
102
103 Returns a tuple consisting of:
104 - server response code (e.g. '250', or such, if all goes well)
105 Note: returns -1 if it can't read responce code.
106 - server response string corresponding to response code
107 (note : multiline responces converted to a single, multiline
108 string)
109
110 """
111 resp=[]
112 self.file = self.sock.makefile('rb')
113 while 1:
114 line = self.file.readline()
115 if self.debuglevel > 0: print 'reply:', `line`
116 resp.append(string.strip(line[4:]))
117 code=line[:3]
118 #check if multiline resp
119 if line[3:4]!="-":
120 break
121 try:
122 errcode = string.atoi(code)
123 except(ValueError):
124 errcode = -1
125
126 errmsg = string.join(resp,"\n")
127 if self.debuglevel > 0:
128 print 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
129 return errcode, errmsg
130
131 def docmd(self, cmd, args=""):
132 """ Send a command, and return it's responce code """
133
134 self.putcmd(cmd,args)
135 (code,msg)=self.getreply()
136 return code
137# std smtp commands
138
139 def helo(self, name=''):
140 """ SMTP 'helo' command. Hostname to send for this command
141 defaults to the FQDN of the local host """
142 name=string.strip(name)
143 if len(name)==0:
144 name=socket.gethostbyaddr(socket.gethostname())[0]
145 self.putcmd("helo",name)
146 (code,msg)=self.getreply()
147 self.helo_resp=msg
148 return code
149
150 def help(self):
151 """ SMTP 'help' command. Returns help text from server """
152 self.putcmd("help")
153 (code,msg)=self.getreply()
154 return msg
155
156 def rset(self):
157 """ SMTP 'rset' command. Resets session. """
158 code=self.docmd("rset")
159 return code
160
161 def noop(self):
162 """ SMTP 'noop' command. Dosen't do anything :> """
163 code=self.docmd("noop")
164 return code
165
166 def mail(self,sender):
167 """ SMTP 'mail' command. Begins mail xfer session. """
168 self.putcmd("mail","from: %s" % sender)
169 return self.getreply()
170
171 def rcpt(self,recip):
172 """ SMTP 'rcpt' command. Indicates 1 recipient for this mail. """
173 self.putcmd("rcpt","to: %s" % recip)
174 return self.getreply()
175
176 def data(self,msg):
177 """ SMTP 'DATA' command. Sends message data to server.
178 Automatically quotes lines beginning with a period per rfc821 """
179 #quote periods in msg according to RFC821
180 # ps, I don't know why I have to do it this way... doing:
181 # quotepat=re.compile(r"^[.]",re.M)
182 # msg=re.sub(quotepat,"..",msg)
183 # should work, but it dosen't (it doubles the number of any
184 # contiguous series of .'s at the beginning of a line,
185 # instead of just adding one. )
186 quotepat=re.compile(r"^[.]+",re.M)
187 def m(pat):
188 return "."+pat.group(0)
189 msg=re.sub(quotepat,m,msg)
190 self.putcmd("data")
191 (code,repl)=self.getreply()
192 if self.debuglevel >0 : print "data:", (code,repl)
193 if code <> 354:
194 return -1
195 else:
196 self.send(msg)
197 self.send("\n.\n")
198 (code,msg)=self.getreply()
199 if self.debuglevel >0 : print "data:", (code,msg)
200 return code
201
202#some usefull methods
203 def sendmail(self,from_addr,to_addrs,msg):
204 """ This command performs an entire mail transaction.
205 The arguments are:
206 - from_addr : The address sending this mail.
207 - to_addrs : a list of addresses to send this mail to
208 - msg : the message to send.
209
210 This method will return normally if the mail is accepted for at
211 least one recipiant .Otherwise it will throw an exception (either
212 SMTPSenderRefused,SMTPRecipientsRefused, or SMTPDataError)
213
214 That is, if this method does not throw an excception, then someone
215 should get your mail.
216
217 It returns a dictionary , with one entry for each recipient that
218 was refused.
219
220 example:
221
222 >>> import smtplib
223 >>> s=smtplib.SMTP("localhost")
224 >>> tolist= [ "one@one.org",
225 ... "two@two.org",
226 ... "three@three.org",
227 ... "four@four.org"]
228 >>> msg = '''
229 ... From: Me@my.org
230 ... Subject: testin'...
231 ...
232 ... This is a test '''
233 >>> s.sendmail("me@my.org",tolist,msg)
234 { "three@three.org" : ( 550 ,"User unknown" ) }
235 >>> s.quit()
236
237 In the above example, the message was accepted for delivery to three
238 of the four addresses, and one was rejected, with the error code 550.
239 If all addresses are accepted, then the method will return an
240 empty dictionary.
241 """
242
243 if not self.helo_resp:
244 self.helo()
245 (code,resp)=self.mail(from_addr)
246 if code <>250:
247 self.rset()
248 raise SMTPSenderRefused
249 senderrs={}
250 for each in to_addrs:
251 (code,resp)=self.rcpt(each)
252 if (code <> 250) and (code <> 251):
253 senderr[each]=(code,resp)
254 if len(senderrs)==len(to_addrs):
255 #th' server refused all our recipients
256 self.rset()
257 raise SMTPRecipientsRefused
258 code=self.data(msg)
259 if code <>250 :
260 self.rset()
261 raise SMTPDataError
262 #if we got here then somebody got our mail
263 return senderrs
264
265
266 def close(self):
267 """Close the connection to the SMTP server."""
268 if self.file:
269 self.file.close()
270 self.file = None
271 if self.sock:
272 self.sock.close()
273 self.sock = None
274
275
276 def quit(self):
277 self.docmd("quit")
278 self.close()