├── LICENSE ├── README.md ├── examples ├── example_basic.py ├── example_mail_from.py └── example_ssl.py └── umail.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shawwwn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µMail (MicroMail) 2 | A lightweight, scalable SMTP client for sending email in MicroPython. 3 | 4 | 5 | ## Usage: 6 | 7 | A bare minimal approach 8 | 9 | ```py 10 | import umail 11 | smtp = umail.SMTP('smtp.gmail.com', 587, username='my@gmail.com', password='mypassword') 12 | smtp.to('someones@gmail.com') 13 | smtp.send("This is an example.") 14 | smtp.quit() 15 | ``` 16 | 17 | 18 | ## API docs: 19 | 20 | * **`umail.SMTP(host, port, [ssl, username, password])`** 21 | * **host** - smtp server 22 | * **port** - server's port number 23 | * **ssl** - set `True` when SSL is required by the server 24 | * **username** - my username/email to the server 25 | * **password** - my password 26 | 27 | * **`SMTP.login(username, password)`** 28 | If you did not login when intializing the server, do it here! 29 | 30 | * **`SMTP.to(addrs, mail_from)`** 31 | * **addrs** - Recipient's email address. If multiple recipents, use a list, eg. `['aaa@mail.com', 'bbb@mail.com']` 32 | * **mail_from** - manually specify the MAIL FROM address, default value is your smtp username. [example](examples/example_mail_from.py) 33 | 34 | * **`SMTP.write(content)`** 35 | To send a long email or an email that contains large attachments, you will most likely exceed the memory limit of your MCU.\ 36 | Use this function to break up your email into smaller chunks.\ 37 | Each call to `write()` will cause the current `content` to be sent to the server so you can load the next chunk. 38 | 39 | * **`SMTP.send([content])`** 40 | Finish writing the email.\ 41 | Make the SMTP server to actually send your email to the recipent address. 42 | 43 | * **`SMTP.quit()`** 44 | Close the connection to the server 45 | 46 | 47 | ## Other 48 | 49 | For more details, pleasse refer to sample code under `examples\` 50 | -------------------------------------------------------------------------------- /examples/example_basic.py: -------------------------------------------------------------------------------- 1 | # 2 | # An bare minimium example for sending a email 3 | # without SSL connection 4 | # 5 | 6 | import umail 7 | smtp = umail.SMTP('smtp.gmail.com', 587, username='my@gmail.com', password='mypassword') 8 | smtp.to('someones@gmail.com') 9 | smtp.send("This is an example.") 10 | smtp.quit() 11 | -------------------------------------------------------------------------------- /examples/example_mail_from.py: -------------------------------------------------------------------------------- 1 | # 2 | # Setting the MAIL FROM address 3 | # 4 | # Some services may require you to set the MAIL FROM address different than your 5 | # login username. In such cases, you can manually specify the address by 6 | # smtp.to(mail_from=address) 7 | # If argument not set, MAIL FROM address will be default to your login username. 8 | # 9 | # Read more about MAIL FROM: 10 | # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/mail-from.html 11 | # 12 | 13 | import umail 14 | smtp = umail.SMTP('email-smtp.us-west-2.amazonaws.com', 587, username='myusername', password='mypassword') 15 | smtp.to('someones@gmail.com', mail_from='my@gmail.com') 16 | smtp.send("This is an example.") 17 | smtp.quit() 18 | -------------------------------------------------------------------------------- /examples/example_ssl.py: -------------------------------------------------------------------------------- 1 | # 2 | # An example for sending a long email 3 | # with SSL connection 4 | # 5 | # NOTE: 6 | # If the email is too long to fit in an variable, 7 | # you may use write() to send a chunk of the email 8 | # each time. 9 | # 10 | 11 | import umail 12 | smtp = umail.SMTP('smtp.gmail.com', 465, ssl=True) # Gmail's SSL port 13 | smtp.login('bob@gmail.com', 'bobspassword') 14 | smtp.to('alice@gmail.com') 15 | smtp.write("From: Bob \n") 16 | smtp.write("To: Alice \n") 17 | smtp.write("Subject: Poem\n\n") 18 | smtp.write("Roses are red.\n") 19 | smtp.write("Violets are blue.\n") 20 | smtp.write("...\n") 21 | smtp.send() 22 | smtp.quit() 23 | -------------------------------------------------------------------------------- /umail.py: -------------------------------------------------------------------------------- 1 | # uMail (MicroMail) for MicroPython 2 | # Copyright (c) 2018 Shawwwn 3 | # License: MIT 4 | import socket 5 | from ssl import wrap_socket as ssl_wrap_socket 6 | 7 | DEFAULT_TIMEOUT = 10 # sec 8 | LOCAL_DOMAIN = '127.0.0.1' 9 | CMD_EHLO = 'EHLO' 10 | CMD_STARTTLS = 'STARTTLS' 11 | CMD_AUTH = 'AUTH' 12 | CMD_MAIL = 'MAIL' 13 | AUTH_PLAIN = 'PLAIN' 14 | AUTH_LOGIN = 'LOGIN' 15 | 16 | class SMTP: 17 | def cmd(self, cmd_str): 18 | sock = self._sock; 19 | sock.write('%s\r\n' % cmd_str) 20 | resp = [] 21 | next = True 22 | while next: 23 | code = sock.read(3) 24 | next = sock.read(1) == b'-' 25 | resp.append(sock.readline().strip().decode()) 26 | return int(code), resp 27 | 28 | def __init__(self, host, port, ssl=False, username=None, password=None): 29 | self.username = username 30 | addr = socket.getaddrinfo(host, port)[0][-1] 31 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | sock.settimeout(DEFAULT_TIMEOUT) 33 | sock.connect(addr) 34 | if ssl: 35 | sock = ssl_wrap_socket(sock) 36 | code = int(sock.read(3)) 37 | sock.readline() 38 | assert code==220, 'cant connect to server %d, %s' % (code, resp) 39 | self._sock = sock 40 | 41 | code, resp = self.cmd(CMD_EHLO + ' ' + LOCAL_DOMAIN) 42 | assert code==250, '%d' % code 43 | if not ssl and CMD_STARTTLS in resp: 44 | code, resp = self.cmd(CMD_STARTTLS) 45 | assert code==220, 'start tls failed %d, %s' % (code, resp) 46 | self._sock = ssl_wrap_socket(sock) 47 | 48 | if username and password: 49 | self.login(username, password) 50 | 51 | def login(self, username, password): 52 | self.username = username 53 | code, resp = self.cmd(CMD_EHLO + ' ' + LOCAL_DOMAIN) 54 | assert code==250, '%d, %s' % (code, resp) 55 | 56 | auths = None 57 | for feature in resp: 58 | if feature[:4].upper() == CMD_AUTH: 59 | auths = feature[4:].strip('=').upper().split() 60 | assert auths!=None, "no auth method" 61 | 62 | from ubinascii import b2a_base64 as b64 63 | if AUTH_PLAIN in auths: 64 | cren = b64("\0%s\0%s" % (username, password))[:-1].decode() 65 | code, resp = self.cmd('%s %s %s' % (CMD_AUTH, AUTH_PLAIN, cren)) 66 | elif AUTH_LOGIN in auths: 67 | code, resp = self.cmd("%s %s %s" % (CMD_AUTH, AUTH_LOGIN, b64(username)[:-1].decode())) 68 | assert code==334, 'wrong username %d, %s' % (code, resp) 69 | code, resp = self.cmd(b64(password)[:-1].decode()) 70 | else: 71 | raise Exception("auth(%s) not supported " % ', '.join(auths)) 72 | 73 | assert code==235 or code==503, 'auth error %d, %s' % (code, resp) 74 | return code, resp 75 | 76 | def to(self, addrs, mail_from=None): 77 | mail_from = self.username if mail_from==None else mail_from 78 | code, resp = self.cmd('MAIL FROM: <%s>' % mail_from) 79 | assert code==250, 'sender refused %d, %s' % (code, resp) 80 | 81 | if isinstance(addrs, str): 82 | addrs = [addrs] 83 | count = 0 84 | for addr in addrs: 85 | code, resp = self.cmd('RCPT TO: <%s>' % addr) 86 | if code!=250 and code!=251: 87 | print('%s refused, %s' % (addr, resp)) 88 | count += 1 89 | assert count!=len(addrs), 'recipient refused, %d, %s' % (code, resp) 90 | 91 | code, resp = self.cmd('DATA') 92 | assert code==354, 'data refused, %d, %s' % (code, resp) 93 | return code, resp 94 | 95 | def write(self, content): 96 | self._sock.write(content) 97 | 98 | def send(self, content=''): 99 | if content: 100 | self.write(content) 101 | self._sock.write('\r\n.\r\n') # the five letter sequence marked for ending 102 | line = self._sock.readline() 103 | return (int(line[:3]), line[4:].strip().decode()) 104 | 105 | def quit(self): 106 | self.cmd("QUIT") 107 | self._sock.close() 108 | --------------------------------------------------------------------------------