Теперь объединим вместе все, что мы узнали о получении, отправке, анализе и составлении сообщений электронной почты, в простом, но функциональном инструменте командной строки для электронной почты. Сценарий в примере 13.20 реализует интерактивный сеанс электронной почты — пользователи могут вводить команды для чтения, отправки и удаления электронных писем. Для получения и отправки писем он использует модули poplib и smtplib, а для анализа и составления новых сообщений — пакет email.
Пример 13.20. PP4E\Internet\Email\pymail.py
#!/usr/local/bin/python """
########################################################################## pymail — простой консольный клиент электронной почты на языке Python;
использует модуль Python poplib для получения электронных писем, smtplib для отправки новых писем и пакет email для извлечения заголовков с содержимым и составления новых сообщений;
########################################################################## import poplib, smtplib, email.utils, mailconfig
from email.parser import Parser
from email.message import Message
fetchEncoding = mailconfig.fetchEncoding
def decodeToUnicode(messageBytes, fetchEncoding=fetchEncoding)
4E, Py3.1: декодирует извлекаемые строки bytes в строки str Юникода для отображения или анализа; использует глобальные настройки (или значения по умолчанию для платформы, исследует заголовки, делает обоснованные предположения); в Python 3.2/3.3 этот шаг может оказаться необязательным: в этом случае достаточно будет просто вернуть сообщение нетронутым;
return [line.decode(fetchEncoding) for line in messageBytes] def splitaddrs(field):
4E: разбивает список адресов по запятым, учитывает возможность появления запятых в именах
pairs = email.utils.getaddresses([field]) # [(name,addr)]
return [email.utils.formataddr(pair) for pair in pairs] # [name <addr>]
def inputmessage():
import sys
From = input(‘From? ‘).strip()
To = input(‘To? ‘).strip() # заголовок Date
# устанавливается автоматически
To = splitaddrs(To) # допускается множество, name+<addr>
Subj = input(‘Subj? ‘).strip() # не разбивать вслепую по ‘,’ или ‘;’
print(‘Type message text, end with line="."’) text = » while True:
line = sys.stdin.readline()
if line == ‘.\n’: break text += line
return From, To, Subj, text
def sendmessage():
From, To, Subj, text = inputmessage()
msg = Message()
msg[‘From’] = From
msg[‘To’] = ‘, ‘.join(To) # для заголовка, не для отправки msg[‘Subject’] = Subj
msg[‘Date’] = email.utils.formatdate() # текущие дата
# и время, rfc2822
msg.set_payload(text)
server = smtplib.SMTP(mailconfig.smtpservername)
try:
failed = server.sendmail(From, To, str(msg)) # может также
except: # возбудить исключение
print(‘Error — send failed’)
else:
if failed: print(‘Failed:’, failed)
def connect(servername, user, passwd):
print(‘Connecting…’)
server = poplib.POP3(servername)
server.user(user) # соединиться, зарегистрироваться на сервере
server.pass_(passwd) # pass — зарезервированное слово
print(server.getwelcome()) # print выведет возвращаемое приветствие return server
def loadmessages(servername, user, passwd, loadfrom=1):
server = connect(servername, user, passwd)
try:
print(server.list())
(msgCount, msgBytes) = server.stat()
print(‘There are’, msgCount, ‘mail messages in’, msgBytes, ‘bytes’) print(‘Retrieving…’)
msgList = [] # получить почту
for i in range(loadfrom, msgCount+1): # пусто, если low >= high (hdr, message, octets) = server.retr(i) # сохранить текст # в списке
message = decodeToUnicode(message) # 4E, Py3.1: bytes в str
msgList.append(‘\n’.join(message)) # оставить письмо на сервере finally:
server.quit() # разблокировать почтовый ящик
assert len(msgList) == (msgCount — loadfrom) + 1 # нумерация с 1 return msgList
def deletemessages(servername, user, passwd, toDelete, verify=True): print(‘To be deleted:’, toDelete)
if verify and input(‘Delete?’)[:1] not in [‘y’, ‘Y’]:
print(‘Delete cancelled.’) else:
server = connect(servername, user, passwd) try:
print(‘Deleting messages from server…’) for msgnum in toDelete: # повторно соединиться
# для удаления писем
server.dele(msgnum) # ящик будет заблокирован
# до вызова quit() finally:
server.quit()
def showindex(msgList): count = 0 # вывести некоторые заголовки
for msgtext in msgList:
msghdrs = Parser().parsestr(msgtext, headersonly=True) # ожидается # тип
count += 1 # str в 3.1
print(‘%d:\t%d bytes’ % (count, len(msgtext))) for hdr in (‘From’, ‘To’, ‘Date’, ‘Subject’): try:
print(‘\t%-8s=>%s’ % (hdr, msghdrs[hdr])) except KeyError:
print(‘\t%-8s=>(unknown)’ % hdr) if count % 5 == 0:
input(‘[Press Enter key]’) # приостановка через каждые 5 писем
def showmessage(i, msgList):
if 1 <= i <= len(msgList):
#print(msgList[i-1]) # устар.: вывести целиком — заголовки+текст print(‘-‘ * 79)
msg = Parser().parsestr(msgList[i-1]) # ожидается тип str в 3.1 content = msg.get_payload() # содержимое: строка
# или [Messages]
if isinstance(content, str): # сохранить только самый
content = content.rstrip() + ‘\n’ # последний символ
# конца строки print(content)
print(‘-‘ * 79) # получить только текст, см. email.parsers
else:
print(‘Bad message number’)
def savemessage(i, mailfile, msgList):
if 1 <= i <= len(msgList):
savefile = open(mailfile, ‘a’, encoding=mailconfig.fetchEncoding) # 4E savefile.write(‘\n’ + msgList[i-1] + ‘-‘*80 + ‘\n’) else:
print(‘Bad message number’)
def msgnum(command): try:
return int(command.split()[1]) except:
return -1 # предполагается, что это ошибка
helptext = """
Available commands:
i — index display
l n? — list all messages (or just message n)
d n? — mark all messages for deletion (or just message n)
s n? — save all messages to a file (or just message n)
m — compose and send a new mail message q — quit pymail
? — display this help text
def interact(msgList, mailfile):
showindex(msgList)
toDelete = [] while True:
try:
command = input(‘[Pymail] Action? (i, l, d, s, m, q, ?) ‘) except EOFError:
command = ‘q’
if not command: command = ‘*’
#завершение if command == ‘q‘: break
# оглавление
elif command[0] == ‘i’: showindex(msgList)
# содержимое письма elif command[0] == ‘l‘:
if len(command) == 1:
for i in range(1, len(msgList)+1): showmessage(i, msgList) else:
showmessage(msgnum(command), msgList)
# сохранение
elif command[0] == ‘s’:
if len(command) == 1:
for i in range(1, len(msgList)+1):
savemessage(i, mailfile, msgList)
else:
savemessage(msgnum(command), mailfile, msgList)
# удаление
elif command[0] == ‘d’
if len(command) == 1: # удалить все позднее
toDelete = list(range(1, len(msgList)+1)) # в 3.x требуется else: # вызвать list()
delnum = msgnum(command) if (1 <= delnum <= len(msgList))
and (delnum not in toDelete): toDelete.append(delnum)
else: print(‘Bad message number’)
# составление нового письма
elif command[0] == ‘m‘: # отправить новое сообщение
# через SMTP sendmessage()
#execfile(‘smtpmail.py‘, {}) # альтернатива: запустить
# в собственном пространстве имен
elif command[0] == ‘?’:
print(helptext)
else:
print(‘What? — type "?" for commands help’) return toDelete
if __name__ == ‘__main__’:
import getpass, mailconfig
mailserver = mailconfig.popservername # например: ‘pop.rmi.net’ mailuser = mailconfig.popusername # например: ‘lutz’
mailfile = mailconfig.savemailfile # например: r’c:\stuff\savemail’
mailpswd = getpass.getpass(‘Password for %s?’ % mailserver) print(‘[Pymail email client]’) msgList = loadmessages(mailserver, mailuser, mailpswd) # загрузить все toDelete = interact(msgList, mailfile) if toDelete: deletemessages(mailserver, mailuser, mailpswd, toDelete) print(‘Bye.’)
Нового здесь немного — просто сочетание логики интерфейса пользователя, уже знакомых нам инструментов и некоторых новых приемов:
Загрузка
Этот клиент загружает с сервера всю электронную почту в находящийся в оперативной памяти список Python только один раз, при начальном запуске. Чтобы получить вновь поступившую почту, необходимо завершить программу и запустить ее снова.
Сохранение
По требованию сценарий pymail сохраняет необработанный текст выбранного сообщения в локальном файле, имя которого указано в модуле mailconfig из примера 13.17.
Удаление
Теперь, наконец, поддерживается удаление почты с сервера по соответствующему запросу: сценарий pymail позволяет выбирать письма для удаления по номерам, но все же физически они удаляются с сервера только при выходе и только при подтверждении операции. Благодаря удалению только при выходе из программы удается избежать изменения номеров почтовых сообщений во время сеанса — в POP удаление почты из середины списка ведет к уменьшению номеров всех сообщений, следующих за тем, которое удаляется. Так как pymail кэширует все сообщения в памяти, последующие операции с пронумерованными сообщениями в памяти могут быть неправильно применены, если осуществлять удаления незамедлительно.
Анализ и составление сообщений
По командам вывода сообщений сценарий pymail выводит только содержательный текст сообщения, а не весь исходный текст, а при выводе оглавления почтового ящика отображаются только выбранные заголовки, выделенные из каждого сообщения. Для извлечения заголовков и содержимого писем используется пакет email, как показано в предыдущем разделе. Кроме того, сценарий использует пакет email также для составления сообщений, запрашивая строку, которая будет отправлена в виде письма.
Я думаю, что сейчас вы уже достаточно хорошо знаете язык Python, чтобы прочесть этот сценарий и разобраться, как он работает, поэтому вместо лишних слов о его устройстве перейдем к интерактивному сеансу pymail и посмотрим его в действии.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, II том, 2011