SharedNames: глобальные переменные программы

sharednames globalnye peremennye programmy Почтовый клиент PyMailGUI Python

Модуль в примере 14.2 реализует общее, глобальное для программы пространство имен, в котором собраны ресурсы, используемые в большинстве модулей программы, и определяются глобальные объекты, объединяющие модули. Позволяет другим модулям избежать избыточного повторения инструкций импортирования общих модулей и инкапсулирует инструкции импортирования пакета — это единственный модуль, куда придется вносить изменения, если пути к каталогам изменятся в будущем. Использование глобальных переменных в целом может сделать программу более сложной для понимания (местоположение некоторых имен может быть неочевидным), но это вполне разумный шаг, если все такие имена собраны в единственном модуле, как этот, потому что это единственное место, где придется искать неизвестные имена.

Пример 14.2. PP4E\Internet\Email\PyMailGui\SharedNames.py

############################################################################ объекты, совместно используемые всеми классами окон и главным модулем: глобальные переменные

############################################################################

#  используется во всех окнах, заголовки appname = PyMailGUI 3.0′

#  используется операциями сохранения, открытия, удаления из списка;

#  а также для файла, куда сохраняются отправленные сообщения saveMailSeparator = PyMailGUI+ (‘-‘*60) + PyMailGUI\n

#  просматриваемые в настоящее время файлы; а также для файла,

#  куда сохраняются отправленные сообщения

openSaveFiles = {} # 1 окно на файл,{имя:окно}

#  службы стандартной библиотеки

import sys, os, email.utils, email.message, webbrowser, mimetypes from tkinter import *

from tkinter.simpledialog import askstring

from tkinter.filedialog import SaveAs, Open, Directory

from tkinter.messagebox import showinfo, showerror, askyesno

#   повторно используемые примеры из книги

from PP4E.Gui.Tools import windows # рамка окна, протокол

# завершения

from PP4E.Gui.Tools import threadtools # очередь обработчиков в потоках

from PP4E.Internet.Email import mailtools # утилиты загрузки, отправки,

# анализа, создания

from PP4E.Gui.TextEditor import textEditor # компонент и окно

#   модули, определяемые здесь

import mailconfig # пользовательские параметры: серверы, шрифты и т.д.

import popuputil # диалоги вывода справки, инф. о ходе выполнения операции

import wraplines # перенос длинных строк

import messagecache # запоминает уже загруженную почту

import html2text # упрощенный механизм преобразования html-Хекст

import PyMailGuiHelp # документация пользователя

def printStack(exc_info):

отладка: выводит информацию об исключении и трассировку стека в stdout; 3.0: выводит трассировочную информацию в файл журнала, если вывод в sys.stdout терпит неудачу: это происходит при запуске из другой программы в Windows; без этого обходного решения PyMailGUI аварийно завершает работу, поскольку вызывается из главного потока при появлении исключения в дочернем потоке; вероятно, ошибка в Python 3.1:

эта проблема отсутствует в версиях 2.5 и 2.6, и объект с трассировочной информацией прекрасно работает, если вывод осуществляется в файл; по иронии, простой вызов print() здесь тоже работает (но вывод отправляется в никуда) при запуске из другой программы;

print(exc_info[0])

print(exc_info[1])

import traceback

try: # ok, если

traceback.print_tb(exc_info[2], file=sys.stdout) # не запущена except: # из другой программы!

log = open(‘_pymailerrlog.txt’, ‘a’) # использовать файл

log.write(‘-‘*80) # иначе завершится

traceback.print_tb(exc_info[2], file=log) # в 3.X, но не в 2.5/6

#   счетчики рабочих потоков выполнения, запущенных этим графич. интерфейсом

#   sendingBusy используется всеми окнами отправки,

# используется операцией завершения главного окна

loadingHdrsBusy = threadtools.ThreadCounter() # только 1

deletingBusy = threadtools.ThreadCounter() # только 1

loadingMsgsBusy = threadtools.ThreadCounter() # может быть множество

sendingBusy = threadtools.ThreadCounter() # может быть множество

ListWindows: окна со списками сообщений

Пример 14.3 реализует окна со списками оглавления почты — окно для отображения содержимого почтового ящика на сервере и одно или более окон с содержимым локальных файлов. Окна этих двух типов в значительной степени выглядят и действуют одинаково и фактически выполняют общий программный код в суперклассе. Подклассы окон главным образом всего лишь специализируют суперкласс, отображая операции загрузки и удаления сообщений для работы с сервером или с локальным файлом.

Окна со списками создаются при запуске программы (начальное окно со списком сообщений на сервере и, возможно, окна со списками сообщений в файлах, имена которых передаются в виде аргументов командной строки), а также в ответ на нажатие кнопки Open (Открыть) в существующем окне со списком (чтобы открыть новое окно со списком сообщений в файле). Программный код инициализации окна можно найти в обработчике кнопки Open (Открыть) в этом примере.

Обратите внимание, что базовые операции обработки почты из пакета mailtools из главы 13 подмешиваются в PyMailGUI различными способами. Классы окон со списками в примере 14.3 наследуют из пакета mailtools класс MailParser, а окно со списком сообщений на сервере встраивает экземпляр объекта кэша сообщений, который в свою очередь наследует из пакета mailtools класс MailFetcher. Класс MailSender из пакета mailtools наследуется окнами просмотра и создания сообщений, но не наследуется окнами со списками. Окна просмотра также наследуют класс MailParser.

Это довольно длинный файл. В принципе, его можно было бы разбить на три файла, по одному на каждый класс, но эти классы настолько тесно связаны между собой, что для разработки было удобнее поместить их в один файл. По сути это один класс с двумя небольшими расширениями.

Пример 14.3. PP4E\Internet\Email\PyMailGui\ListWindows.py

############################################################################ Реализация окон со списками сообщений на почтовом сервере и в локальных файлах: по одному классу для каждого типа окон. Здесь использован прием выделения общего программного кода для повторного использования: окна со списками содержимого на сервере и в файлах являются специализированными версиями класса PyMailCommon окон со списками; класс окна со списком почты на сервере отображает свои операции на операции получения почты с сервера, класс окна со списком почты в файле отображает свои операции на операции с локальным файлом.

В ответ на действия пользователя окна со списками создают окна просмотра, создания, ответа и пересылки писем. Окно со списком сообщений на сервере играет роль главного окна и открывается при запуске сценарием верхнего уровня; окна со списками сообщений в файлах открываются по требованию, щелчком на кнопке "Open" в окне со списком сообщений на сервере или в файле. Номера сообщений могут временно рассинхронизироваться с сервером, в случае изменения содержимого почтового ящика входящих сообщений (вызывает полную перезагрузку списка).

Изменения в этом модуле в версии 2.1:

-теперь проверяет синхронизации номеров сообщений при удалении и загрузке

-добавляет в окна просмотра до N кнопок прямого доступа к вложениям

-загрузка из файлов выполняется в отдельном потоке, чтобы избежать задержек при загрузке больших файлов

-удаление отправленных сообщений из файла также выполняется в потоке, благодаря чему исчезла задержка в работе графического интерфейса

Что сделать:

-сохранение отправленных сообщений в файл до сих пор выполняется в главном потоке: это может вызывать короткие паузы в работе графического интерфейса, хотя это маловероятно — в отличие от загрузки и удаления, сохранение/отправка просто записывает сообщение в конец файла.

-реализация операций с локальными файлами, как с текстовыми файлами с разделителями в значительной степени является лишь прототипом: она загружает полные тексты сообщений целиком в память, что ограничивает возможный размер этих файлов; лучшее решение: использовать 2 DBM-файла с доступом по ключу — для заголовков и для полного текста сообщений, плюс список для отображения ключей в позиции; в такой реализации файлы с сообщениями превращаются в каталоги и становятся нечитаемыми для человека.

############################################################################

from SharedNames import * # глобальные объекты программы

from ViewWindows import ViewWindow, WriteWindow, ReplyWindow, ForwardWindow

############################################################################

#  основной фрейм — общая структура списков с сообщениями на сервере

#  и в файлах

############################################################################

class PyMailCommon(mailtools.MailParser):

абстрактный пакет виджетов с главным списком сообщений;

смешивается с классами Tk, Toplevel или Frame окон верхнего уровня; должен специализироваться в подклассах с помощью метода actions() и других; создает окна просмотра и создания по требованию:

они играют роль MailSenders;

# атрибуты класса, совместно используемые всеми окнами со списками threadLoopStarted = False # запускается первым окном

queueChecksPerSecond = 20 # изменит, в зависимости

#  от нагрузки на процессор

queueDelay = 1000 // queueChecksPerSecond # минимальное число миллисекунд

#  между событиями таймера

queueBatch = 5 # максимальное число вызовов

#   обработчиков на одно событие

#   от таймера

#   все окна используют одни и те же диалоги: запоминают последний каталог openDialog = Open(title=appname + ‘: Open Mail File‘)

saveDialog = SaveAs(title=appname + ‘: Append Mail File’)

#   3.0: чтобы избежать загрузки одного и того же сообщения # в разных потоках beingFetched = set()

def __init__(self):

self.makeWidgets() # нарисовать содержимое окна: список, панель if not PyMailCommon.threadLoopStarted:

#   запустить цикл проверки завершения потоков

#   цикл событий от таймера, в котором производится вызов

#   обработчиков из очереди;

#   один цикл для всех окон: окна со списками с сервера

#   и из файла, окна просмотра могут выполнять операции в потоках;

#   self — экземпляр класса Tk, Toplevel или Frame: достаточно

#   будет виджета любого типа;

#   3.0/4E: для увеличения скорости обработки увеличено количество

#   вызываемых обработчиков и количество событий в секунду:

#   ~100x/sec;

PyMailCommon.threadLoopStarted = True

threadtools.threadChecker(self, self.queueDelay, self.queueBatch)

def makeWidgets(self):

#   добавить флажок "All" внизу

tools = Frame(self, relief=SUNKEN, bd=2, cursor=’hand2′) # 3.0: настр. tools.pack(side=BOTTOM, fill=X) self.allModeVar = IntVar()

chk = Checkbutton(tools, text="All") chk.config(variable=self.allModeVar, command=self.onCheckAll) chk.pack(side=RIGHT)

#   добавить кнопки на панель инструментов внизу for (title, callback) in self.actions(): if not callback:

sep = Label(tools, text=title) # 3.0: разделитель

sep.pack(side=LEFT, expand=YES, fill=BOTH) # растет с окном else:

Button(tools, text=title, command=callback).pack(side=LEFT)

#   добавить список с возможностью выбора нескольких записей

# и полосами прокрутки

listwide = mailconfig.listWidth or 74 # 3.0: настр. нач. размера

listhigh = mailconfig.listHeight or 15 # ширина=символы, высота=строки

mails = Frame(self)

vscroll = Scrollbar(mails)

hscroll = Scrollbar(mails, orient=’horizontal’)

fontsz = (sys.platform[:3] == ‘win’ and 8) or 10 # по умолчанию

listbg = mailconfig.listbg or ‘white’

listfg = mailconfig.listfg or ‘black’

listfont = mailconfig.listfont or (‘courier’, fontsz, ‘normal’) listbox = Listbox(mails, bg=listbg, fg=listfg, font=listfont) listbox.config(selectmode=EXTENDED)

listbox.config(width=listwide, height=listhigh) # 3.0: иниц. шире listbox.bind(‘<Double-1>’, (lambda event: self.onViewRawMail()))

#   связать список и полосы прокрутки

vscroll.config(command=listbox.yview, relief=SUNKEN) hscroll.config(command=listbox.xview, relief=SUNKEN) listbox.config(yscrollcommand=vscroll.set, relief=SUNKEN) listbox.config(xscrollcommand=hscroll.set)

#   присоединяется последним = усекается первым

mails.pack(side=TOP, expand=YES, fill=BOTH)

vscroll.pack(side=RIGHT, fill=BOTH)

hscroll.pack(side=BOTTOM, fill=BOTH)

listbox.pack(side=LEFT, expand=YES, fill=BOTH) self.listBox = listbox

#####################

# обработчики событий

#####################

def onCheckAll(self):

#   щелчок на флажке "All"

if self.allModeVar.get():

self.listBox.select_set(0, END)

else:

self.listBox.select_clear(0, END)

def onViewRawMail(self):

#   может вызываться из потока: просмотр выбранных сообщений —

#   необработанный текст заголовков, тела

msgnums = self.verifySelectedMsgs()

if msgnums:

self.getMessages(msgnums, after=lambda: self.contViewRaw(msgnums))

def contViewRaw(self, msgnums, pyedit=True): # необх. полный TextEditor?

for msgnum in msgnums: # может быть вложенное определение

fulltext = self.getMessage(msgnum) # полный текст — декодир. Юникод if not pyedit:

#   вывести в виджете scrolledtext

from tkinter.scrolledtext import ScrolledText

window = windows.QuietPopupWindow(appname,

‘raw message viewer’)

browser = ScrolledText(window) browser.insert(‘0.0’, fulltext) browser.pack(expand=YES, fill=BOTH) else:

#  3.0/4E: более полноценный текстовый редактор PyEdit wintitle = ‘ — raw message text’

browser = textEditor.TextEditorMainPopup(self,

winTitle=wintitle)

browser.update()

browser.setAllText(fulltext)

browser.clearModified()

def onViewFormatMail(self):

может вызываться из потока: просмотр отобранных сообщений — выводит форматированное отображение в отдельном окне; вызывается не из потока, если вызывается из списка содержимого файла или сообщения уже были загружены; действие after вызывается, только если предварительное получение в getMessages возможно и было выполнено msgnums = self.verifySelectedMsgs()

if msgnums:

self.getMessages(msgnums, after=lambda: self.contViewFmt(msgnums))

def contViewFmt(self, msgnums):

завершение операции вывода окна просмотра: извлекает основной текст, выводит окно (окна) для отображения; если необходимо, извлекает простой текст из html, выполняет перенос строк; сообщения в формате html: выводит извлеченный текст, затем сохраняет во временном файле и открывает в веб-броузере; части сообщений могут также открываться вручную из окна просмотра с помощью кнопки Split (Разбить) или кнопок быстрого доступа к вложениям; в сообщении, состоящем из единственной части, иначе: часть должна открываться вручную кнопкой Split (Разбить) или кнопкой быстрого доступа к части; проверяет необходимость открытия html в mailconfig;

3.0: для сообщений, содержащих только разметку html, основной текст здесь имеет тип str, но сохраняется он в двоичном режиме, чтобы обойти проблемы с кодировками; это необходимо, потому что значительная часть электронных писем в настоящее время отправляется в формате html; в первых версиях делалась попытка подобрать кодировку из N возможных (utf-8, latin-1, по умолчанию для платформы), но теперь тело сообщения получается и сохраняется в двоичном виде, чтобы минимизировать любые потери точности;

если позднее часть будет открываться по требованию, она точно так же будет сохранена в файл в двоичном режиме;

предупреждение: запускаемый веб-броузер не получает оригинальные заголовки письма: ему, вероятно, придется строить свои догадки о кодировке или вам придется явно сообщать ему о кодировке, если в разметке html отсутствуют собственные заголовки с информацией о кодировке (они принимают форму тегов <meta> в разделе <head>, если таковой имеются; здесь ничего не вставляется в разметку html, так как некоторые корректно оформленные части в формате html уже имеют все необходимое); IE, похоже, способен обработать большинство файлов html; кодирования частей html в utf-8 также может оказаться вполне достаточным: эта кодировка может с успехом применяться к большинству типов текста;

for msgnum in msgnums:

fulltext = self.getMessage(msgnum) # 3.0: str для анализа

message = self.parseMessage(fulltext)

type, content = self.findMainText(message) # 3.0: декод. Юникод if type in [‘text/html’, ‘text/xml’]: # 3.0: извлечь текст

content = html2text.html2text(content)

content = wraplines.wrapText1(content, mailconfig.wrapsz) ViewWindow(headermap = message,

showtext = content,

origmessage = message) # 3.0: декодирует заголовки

# единственная часть, content-type text/HTML (грубо, но верно!) if type == ‘text/html’:

if ((not mailconfig.verifyHTMLTextOpen) or

askyesno(appname, ‘Open message text in browser?’)):

#  3.0: перед декодированием в Юникод

#  преобразовать из формата MIME

type, asbytes = self.findMainText(message, asStr=False) try:

from tempfile import gettempdir # или виджет Tk tempname = os.path.join(gettempdir(),

‘pymailgui.html’) # просмотра HTML? tmp = open(tempname, ‘wb’) # уже кодирован

tmp.close()

tmp.write(asbytes)

webbrowser.open_new(‘file://’ + tempname)

except:

showerror(appname, ‘Cannot open in browser’)

def onWriteMail(self):

окно составления нового письма на пустом месте, без получения других писем; здесь ничего не требуется цитировать, но следует добавить подпись и записать адрес отправителя в заголовок Bcc, если этот заголовок разрешен в mailconfig; значение для заголовка From в mailconfig может быть интернационализированным:

окно просмотра декодирует его;

starttext = ‘\n # использовать текст подписи

if mailconfig.mysignature:

starttext += ‘%s\n’ % mailconfig.mysignature

From = mailconfig.myaddress

WriteWindow(starttext = starttext, # 3.0: заполнить

headermap = dict(From=From, Bcc=From)) # заголовок Bcc

def onReplyMail(self):

# может вызываться из потока: создание ответа на выбранные письма

msgnums = self.verifySelectedMsgs()

if msgnums:

self.getMessages(msgnums, after=lambda: self.contReply(msgnums))

def contReply(self, msgnums):

завершение операции составления ответа: отбрасывает вложения, цитирует текст оригинального сообщения с помощью символов ‘>’, добавляет подпись; устанавливает начальные значения заголовков To/From, беря их из оригинального сообщения или из модуля с настройками; не использует значение оригинального заголовка To для From: может быть несколько адресов или название списка рассылки; заголовок To сохраняет формат имя+<адрес>, даже если в имени используется ‘,’; для заголовка To используется значение оригинального заголовка From, игнорирует заголовок replyto, если таковой имеется; 3.0: копия ответа по умолчанию также отправляется всем получателям оригинального письма;

3.0: чтобы обеспечить возможность использования запятых в качестве разделителей адресов, теперь используются функции getaddresses/parseaddr, благодаря которым корректно обрабатываются запятые, присутствующие в компонентах имен адресов; в графическом интерфейсе адреса в заголовке также разделяются запятыми, мы копируем запятые при отображении заголовков и используем getaddresses для разделения адресов, когда это необходимо; почтовые серверы требуют, чтобы в качестве разделителей адресов использовался символ ‘,’;

больше не использует parseaddr для получения первой пары имя/адрес из результата, возвращаемого getaddresses: используйте полное значение заголовка From для заголовка To;

3.0: здесь предусматривается декодирование заголовка Subject, потому что необходимо получить его текст, но класс окна просмотра, являющийся суперклассом окна редактирования, выполняет декодирование всех отображаемых заголовков (дополнительное декодирование заголовка Subject является пустой операцией); при отправке все заголовки и имена в заголовках с адресами, содержащие символы вне диапазона ASCII, в графическом интерфейсе присутствуют в декодированном виде, но внутри пакета mailtools обрабатываются в кодированном виде; quoteOrigText также декодирует первоначальные значения заголовков, вставляемые в цитируемый текст, и окна со списками декодируют заголовки для отображения;

for msgnum in msgnums:

fulltext = self.getMessage(msgnum) message = self.parseMessage(fulltext) # при ошибке — объект ошибки maintext = self.formatQuotedMainText(message) # то же для пересыл.

# заголовки From и To декодируются окном просмотра From = mailconfig.myaddress # не оригинальный To

To = message.get(‘From’, ») # 3.0: разделитель ‘,’

Cc = self.replyCopyTo(message) # 3.0: всех получателей в cc?

Subj = message.get(‘Subject’, ‘(no subject)’)

Subj = self.decodeHeader(Subj) # декодировать, чтобы

# получить str

if Subj[:4].lower() != re: ‘: # 3.0: объединить

Subj = Re: ‘ + Subj

ReplyWindow(starttext = maintext, headermap =

dict(From=From, To=To, Cc=Cc, Subject=Subj, Bcc=From))

def onFwdMail(self):

#   может вызываться из потока: пересылка выбранных писем

msgnums = self.verifySelectedMsgs()

if msgnums:

self.getMessages(msgnums, after=lambda: self.contFwd(msgnums))

def contFwd(self, msgnums):

завершение операции пересылки письма: отбрасывает вложения, цитирует текст оригинального сообщения с помощью символов ‘>’, добавляет подпись; смотрите примечания по поводу заголовков в методах реализации операции составления ответа; класс окна просмотра, являющийся суперклассом, выполнит декодирование заголовка From;

for msgnum in msgnums:

fulltext = self.getMessage(msgnum)

message = self.parseMessage(fulltext)

maintext = self.formatQuotedMainText(message) # то же для ответа

#   начальное значение From берется из настроек,

#   а не из оригинального письма

From = mailconfig.myaddress # кодированный или нет

Subj = message.get(‘Subject’, ‘(no subject)’)

Subj = self.decodeHeader(Subj) # 3.0: закодируется при отправке

if Subj[:5].lower() != ‘fwd: ‘: # 3.0: объединить

Subj = ‘Fwd: ‘ + Subj

ForwardWindow(starttext = maintext, headermap = dict(From=From, Subject=Subj, Bcc=From))

def onSaveMailFile(self):

сохраняет выбранные письма в файл для просмотра без подключения к Интернету; запрещается, если в текущий момент выполняются операции загрузки/удаления с целевым файлом; также запрещается методом getMessages, если self в текущий момент выполняет операции с другим файлом; метод contSave не поддерживает многопоточный режим выполнения: запрещает все остальные операции;

msgnums = self.selectedMsgs()

if not msgnums:

showerror(appname, ‘No message selected’) else:

#   предупреждение: диалог предупреждает о перезаписи

#   существующего файла

filename = self.saveDialog.show() # атрибут класса

if filename: # не проверять номера

filename = os.path.abspath(filename) # нормализовать / в \ self.getMessages(msgnums,

after=lambda: self.contSave(msgnums, filename))

def contSave(self, msgnums, filename):

#   проверяет возможность продолжения, после возможной загрузки

# сообщений с сервера

if (filename in openSaveFiles.keys() and # этот файл просматривается? openSaveFiles[filename].openFileBusy): # операции загр./удал.? showerror(appname, ‘Target file busy cannot save’)

else:

try: # предупр.: не многопоточн.

fulltextlist = [] # 3.0: использ. кодировку

mailfile = open(filename, ‘a’, encoding=mailconfig.fetchEncoding) for msgnum in msgnums: # < 1 сек. для N сообщ.

fulltext = self.getMessage(msgnum) # но сообщений может if fulltext[-1] != ‘\n’: fulltext += ‘\n’ # быть слишком много mailfile.write(saveMailSeparator) mailfile.write(fulltext)

fulltextlist.append(fulltext)

mailfile.close()

except:

showerror(appname, ‘Error during save’) printStack(sys.exc_info())

else: # почему .keys(): EIBTI

if filename in openSaveFiles.keys(): # этот файл уже открыт?

window = openSaveFiles[filename] # обновить список, чтобы window.addSavedMails(fulltextlist) # избежать повторного откр.

#window.loadMailFileThread() # это было очень медленно

def onOpenMailFile(self, filename=None):

#   обработка сохраненной почты без подключения к Интернету

filename = filename or self.openDialog.show() # общий атрибут класса if filename:

filename = os.path.abspath(filename) # полное имя файла

if filename in openSaveFiles.keys(): # только 1 окно на файл

openSaveFiles[filename].lift() # поднять окно файла,

showinfo(appname, ‘File already open’) # иначе будут возникать else: # ошибки после удаления

from PyMailGui import PyMailFileWindow # избежать дублирования popup = PyMailFileWindow(filename) # новое окно со списком openSaveFiles[filename] = popup # удаляется при закр.

popup.loadMailFileThread() # загрузить в потоке

def onDeleteMail(self):

#   удаляет выбранные письма с сервера или из файла

msgnums = self.selectedMsgs() # подкласс: fillIndex

if not msgnums: # всегда проверять

showerror(appname, ‘No message selected’) else:

if askyesno(appname, ‘Verify delete %d mails?’ % len(msgnums)): self.doDelete(msgnums)

########################

# вспомогательные методы

########################

def selectedMsgs(self):

# возвращает выбранные в списке сообщения

selections = self.listBox.curselection() # кортеж строк-чисел, 0..N-1 return [int(x)+1 for x in selections] # преобр. в int, создает 1..N

warningLimit = 15

def verifySelectedMsgs(self):

msgnums = self.selectedMsgs()

if not msgnums:

showerror(appname, ‘No message selected’) else:

numselects = len(msgnums)

if numselects > self.warningLimit:

if not askyesno(appname, ‘Open %d selections?’ % numselects): msgnums = []

return msgnums

def fillIndex(self, maxhdrsize=25):

заполняет запись в списке содержимым заголовков;

3.1  : декодирует заголовки в соответствии с email/mime/unicode, если необходимо;

3.2  : предупреждение: крупные символы из китайского алфавита могут нарушить выравнивание границ ‘|’ колонок;

hdrmaps = self.headersMaps() # может быть пустым

showhdrs = (‘Subject‘, ‘From‘, ‘Date‘, ‘To‘) # загол-ки по умолчанию if hasattr(mailconfig, ‘listheaders‘): # загол-ки в mailconfig

showhdrs = mailconfig.listheaders or showhdrs

addrhdrs = (‘From’, ‘To’, ‘Cc’, ‘Bcc’) # 3.0: декодируются особо

#   вычислить максимальный размер поля

maxsize = {}

for key in showhdrs:

allLens = [] # слишком большой для списка!

for msg in hdrmaps:

keyval = msg.get(key, ‘ ‘)

if key not in addrhdrs:

allLens.append(len(self.decodeHeader(keyval)))

else:

allLens.append(len(self.decodeAddrHeader(keyval)))

if not allLens: allLens = [1]

maxsize[key] = min(maxhdrsize, max(allLens))

#   заполнить окно списка полями фиксированной ширины с выравниванием

#   по левому краю

self.listBox.delete(0, END) # наличие неск. частей отметить *

for (ix, msg) in enumerate(hdrmaps): # по заг. content-type

msgtype = msg.get_content_maintype() # нет метода is_multipart

msgline = (msgtype == ‘multipart’ and ‘*’) or ‘ ‘

msgline += ‘%03d’ % (ix+1)

for key in showhdrs:

mysize = maxsize[key]

if key not in addrhdrs:

keytext = self.decodeHeader(msg.get(key, ‘ ‘))

else:

keytext = self.decodeAddrHeader(msg.get(key, ‘ ‘))

msgline += ‘ | %-*s’ % (mysize, keytext[:mysize])

msgline += ‘| %.1fK’ % (self.mailSize(ix+1) / 1024)

# 3.0: .0 необяз.

self.listBox.insert(END, msgline)

self.listBox.see(END) # самое свежее сообщение в последней строке

def replyCopyTo(self, message):

3.0: при создании ответа все получатели оригинального письма копируются из заголовков To и Cc в заголовок Cc ответа после удаления дубликатов, и определяется новый отправитель;

могло бы потребоваться декодировать интернационализированные адреса, но окно просмотра уже декодирует их для отображения (при отправке они повторно кодируются). Фильтрация уникальных значений здесь обеспечивается множеством в любом случае, хотя предполагается, что интернационализированный адрес отправителя в mailconfig будет представлен в кодированной форме (иначе здесь он не будет удаляться); здесь допускаются пустые заголовки To и Cc: split вернет пустой список;

if not mailconfig.repliesCopyToAll:

#   ответить только отправителю

Cc = » else:

#   скопировать всех получателей оригинального письма (3.0) allRecipients = (self.splitAddresses(message.get(‘To’, »)) + self.splitAddresses(message.get(‘Cc’, »)))

uniqueOthers = set(allRecipients) set([mailconfig.myaddress]) Cc = ‘, ‘.join(uniqueOthers)

return Cc or ‘?’

def formatQuotedMainText(self, message):

3.0: общий программный код, используемый операциями создания ответа и пересылки: получает декодированный текст, извлекает текст из html, переносит длинные строки, добавляет символы цитирования ‘>’

type, maintext = self.findMainText(message) # 3.0: декод. строка str if type in [‘text/html’, ‘text/xml’]: # 3.0: простой текст

maintext = html2text.html2text(maintext)

maintext = wraplines.wrapText1(maintext, mailconfig.wrapsz-2) # 2=’> ‘ maintext = self.quoteOrigText(maintext, message) # добавить загол. и > if mailconfig.mysignature:

maintext = (‘\n%s\n’ % mailconfig.mysignature) + maintext return maintext

def quoteOrigText(self, maintext, message):

3.0: здесь необходимо декодировать все интернационализированные заголовки, иначе они будут отображаться в цитируемом тексте в кодированной форме email+MIME; decodeAddrHeader обрабатывает один адрес или список адресов, разделенных запятыми; при отправке это может вызвать кодирование полного текста сообщения, но основной текст уже в полностью декодированной форме: мог быть закодирован в любой кодировке;

quoted = ‘\n Original Message \n’

for hdr in (‘From’, ‘To’, ‘Subject’, ‘Date’):

rawhdr = message.get(hdr, ‘?’)

if hdr not in (‘From’, ‘To’):

dechdr = self.decodeHeader(rawhdr) # весь заголовок else:

dechdr = self.decodeAddrHeader(rawhdr) # только имя в адресе quoted += ‘%s: %s\n’ % (hdr, dechdr)

quoted += ‘\n’ + maintext

quoted = ‘\n’ + quoted.replace(‘\n’, ‘\n> ‘)

return quoted

######################

#  требуются подклассам

#  #####################

#  используется операциями просмотра, сохранения,

#  создания ответа, пересылки

def getMessages(self, msgnums, after): # переопределить, если имеется кэш, after() # проверка потока

#  плюс okayToQuit? и все уникальные операции

def getMessage(self, msgnum): assert False # исп. многими: полный текст def headersMaps(self): assert False # fillIndex: список заголовков

def mailSize(self, msgnum): assert False # fillIndex: размер msgnum def doDelete(self): assert False # onDeleteMail: кнопка Delete

############################################################################

#  главное окно — при просмотре сообщений в локальном файле

#  (или в файле отправленных сообщений)

############################################################################

class PyMailFile(PyMailCommon)

специализирует PyMailCommon для просмотра содержимого файла

с сохраненной почтой; смешивается с классами Tk, Toplevel или Frame, добавляет окно списка; отображает операции загрузки, получения, удаления на операции с локальным текстовым файлом;

операции открытия больших файлов и удаления

писем здесь выполняются в потоках;

операции сохранения и отправки выполняются в главном потоке, потому что вызывают лишь добавление в конец файла; сохранение запрещается, если исходный или целевой файл в текущий момент используются операциями загрузки/удаления; операция сохранения запрещает загрузку, удаление, сохранение только потому, что она не выполняется в параллельном потоке (блокирует графический интерфейс);

Что сделать: может потребоваться использовать потоки выполнения и блокировки файлов на уровне операционной системы, если сохранение когда-либо будет выполняться в потоках; потоки выполнения: операции сохранения могли бы запрещать другие операции сохранения в этот же файл с помощью openFileBusy, но файл может открываться не только в графическом интерфейсе; блокировок файлов недостаточно, потому что графический интерфейс тоже обновляется; Что сделать: операция добавления в конец файла с сохраненными почтовыми сообщениями может потребовать использования блокировок на уровне операционной системы: в данной реализации при попытке выполнить отправку во время операций загрузки/удаления пользователь увидит диалог с сообщением об ошибке;

3.0: сейчас файлы с сохраненными почтовыми сообщениями являются текстовыми, их кодировка определяется настройками в модуле mailconfig; вероятно, это не гарантирует поддержку в самом худшем случае применения необычных кодировок или смешивания разных кодировок, но в большинстве своем полный текст почтовых сообщений состоит только из символов ascii, и пакет email в Python 3.1 еще содержит ошибки;

def actions(self):

return [ (‘Open’, self.onOpenMailFile),

(‘Write’, self.onWriteMail),

(‘ ‘, None), # 3.0: разделители

View’,

self.

onViewFormatMail),

Reply’,

self.

onReplyMail),

Fwd’,

self.

onFwdMail),

Save’,

self.

onSaveMailFile),

Delete’

, self.

onDeleteMail),

1

None),

Quit’,

self.

quit) ]

 

def __init__(self, filename):

# вызывающий: выполняет затем loadMailFileThread

PyMailCommon.__init__(self) self.filename = filename

self.openFileBusy = threadtools.ThreadCounter() # 1 файл — 1 окно

def loadMailFileThread(self)

загружает или повторно загружает файл и обновляет окно со списком содержимого; вызывается операциями открытия, на этапе запуска программы и, возможно, операциями отправки, если файл с отправленными сообщениями открыт в настоящий момент;

в файле всегда присутствует первый фиктивный элемент после разбиения текста;

альтернатива: [self.parseHeaders(m) for m in self.msglist]; можно было бы выводить диалог с информацией о ходе выполнения операции, но загрузка небольших файлов выполняется очень быстро;

2.1:  теперь поддерживает многопоточную модель выполнения — загрузка небольших файлов выполняется < 1 сек., но загрузка очень больших файлов может вызывать задержки в работе графического интерфейса; операция сохранения теперь использует addSavedMails для добавления списков сообщений, чтобы повысить скорость, но это не относится к повторной загрузке; все еще вызывается операцией отправки, просто потому что текст сообщения недоступен — требуется провести рефакторинг; удаление также производится в потоке выполнения: предусмотрено предотвращение возможности перекрытия операций открытия и удаления;

if self.openFileBusy:

#  не допускать параллельное выполнение операций

#  открытия/удаления

errmsg = ‘Cannot load, file is busy:\n"%s"’ % self.filename showerror(appname, errmsg)

else:

#self.listBox.insert(END, ‘loading…’) # вызыв. ошибку при щелчке savetitle = self.title() # устанавливается классом окна

self.title(appname + ‘ — ‘ + ‘Loading…’) self.openFileBusy.incr() threadtools.startThread(

action = self.loadMailFile, args = (), context = (savetitle,),

onExit = self.onLoadMailFileExit, onFail = self.onLoadMailFileFail)

def loadMailFile(self):

#   выполняется в потоке, оставляя графический интерфейс активным

#   операции открытия, чтения и механизм анализа могут возбуждать

#   исключения: перехватывается в утилитах работы с потоками

file = open(self.filename, ‘r’,

encoding=mailconfig.fetchEncoding) # 3.0

allmsgs = file.read()

self.msglist = allmsgs.split(saveMailSeparator)[1:] # полный текст

self.hdrlist = list(map(self.parseHeaders, self.msglist)) # объекты сообщений

def onLoadMailFileExit(self, savetitle):

#   успешная загрузка в потоке

self.title(savetitle) # записать имя файла в заголовок окна

self.fillIndex() # обновить граф. интерф.: вып-ся в главн. потоке self.lift() # поднять окно

self.openFileBusy.decr()

def onLoadMailFileFail(self, exc_info, savetitle):

#   исключение в потоке

showerror(appname, ‘Error opening "%s"\n%s\n%s’ %

((self.filename,) + exc_info[:2]))

printStack(exc_info)

self.destroy() # всегда закрывать окно?

self.openFileBusy.decr() # не требуется при уничтожении окна

def addSavedMails(self, fulltextlist):

оптимизация: добавляет вновь сохраняемые сообщения в окна со списками содержимого, загруженного из файлов; в прошлом при сохранении выполнялась перезагрузка всего файла вызовом loadMailThread — это медленно;

должен вызываться только в главном потоке выполнения:

обновляет графический интерфейс; операция отправки по-прежнему вызывает перезагрузку файла с отправленными сообщениями, если он открыт: отсутствует текст сообщения;

self.msglist.extend(fulltextlist)

self.hdrlist.extend(map(self.parseHeaders,

fulltextlist)) # 3.x итератор

self.fillIndex()

self.lift()

def doDelete(self, msgnums):

бесхитростная реализация, но ее вполне достаточно: перезаписывает в файл все неудаленные сообщения; недостаточно просто удалить из self.msglist: изменяются индексы элементов списка;

Py2.3 enumerate(L) — то же самое, что zip(range(len(L)), L)

2.1:  теперь поддерживает многопоточную модель выполнения, иначе может вызывать паузы в работе графического интерфейса при работе с очень большими файлами

if self.openFileBusy:

#   не допускать параллельное выполнение операций

#   открытия/удаления

errmsg = ‘Cannot delete, file is busy:\n"%s"’ % self.filename showerror(appname, errmsg)

else:

savetitle = self.title()

self.title(appname + ‘ — ‘ + ‘Deleting…’) self.openFileBusy.incr()

threadtools.startThread(

action = self.deleteMailFile, args = (msgnums,), context = (savetitle,), onExit = self.onDeleteMailFileExit, onFail = self.onDeleteMailFileFail)

def deleteMailFile(self, msgnums):

#  выполняется в потоке, оставляя графический интерфейс активным indexed = enumerate(self.msglist)

keepers = [msg for (ix, msg) in indexed if ix+1 not in msgnums] allmsgs = saveMailSeparator.join([»] + keepers) file = open(self.filename, ‘w’,

encoding=mailconfig.fetchEncoding) # 3.0 file.write(allmsgs) self.msglist = keepers

self.hdrlist = list(map(self.parseHeaders, self.msglist))

def onDeleteMailFileExit(self, savetitle):

self.title(savetitle)

self.fillIndex() # обновляет граф. интерф.: выполняется в глав. потоке self.lift() # сбросить заголовок, поднять окно self.openFileBusy.decr()

def onDeleteMailFileFail(self, exc_info, savetitle): showerror(appname, ‘Error deleting "%s"\n%s\n%s’ %

((self.filename,) + exc_info[:2]))

printStack(exc_info)

self.destroy() # всегда закрывать окно?

self.openFileBusy.decr() # не требуется при уничтожении окна

def getMessages(self, msgnums, after):

используется операциями просмотра, сохранения, создания ответа, пересылки: потоки загрузки и удаления могут изменять

списки сообщений и заголовков, поэтому следует

запрещать выполнение других операций, безопасность которых
зависит от них; этот метод реализует проверку для
self: для операций сохранения также проверяется целевой файл;

if self.openFileBusy:

errmsg = ‘Cannot fetch, file is busy:\n"%s"’ % self.filename showerror(appname, errmsg)

else:

after() # почта уже загружена

def getMessage(self, msgnum):

return self.msglist[msgnum-1] # полный текст одного сообщения

def headersMaps(self):

return self.hdrlist # объекты email.message.Message

def mailSize(self, msgnum):

return len(self.msglist[msgnum-1])

def quit(self):

#   не выполняет завершение в ходе обновления:

#   следом вызывается fillIndex

if self.openFileBusy:

showerror(appname, ‘Cannot quit during load or delete’) else:

if askyesno(appname, ‘Verify Quit Window?’):

# удалить файл из списка открытых файлов

del openSaveFiles[self.filename]

Toplevel.destroy(self)

############################################################################

# главное окно — при просмотре сообщений на почтовом сервере

############################################################################

class PyMailServer(PyMailCommon)

специализирует PyMailCommon для просмотра почты на сервере;

подпись: def actions(self): return [ ('load', ('open', ('write', (' ', ('view', ('reply', ('fwd', ('save',смешивается с классами Tk, Toplevel или Frame, добавляет окно со списком сообщений; отображает операции загрузки, получения, удаления на операции с почтовым ящиком на сервере; встраивает объект класса MessageCache, который является наследником класса MailFetcher из пакета mailtools;

self.onLoadServer),

self.onOpenMailFile),

self.onWriteMail),

None), # 3.0: разделители

self.onViewFormatMail),

self.onReplyMail),

self.onFwdMail),

self.onSaveMailFile),

(‘Delete’, self.onDeleteMail),

(‘ ‘, None),

(‘Quit’, self.quit) ]

def __init__(self):

PyMailCommon.__init__(self)

self.cache = messagecache.GuiMessageCache() # встраивается, не наслед.

#self.listBox.insert(END, ‘Press Load to fetch mail’)

def makeWidgets(self): # полоса вызова справки: только в главном окне

self.addHelpBar()

PyMailCommon.makeWidgets(self)

def addHelpBar(self):

msg = ‘PyMailGUI a Python/tkinter email client (help)’

title = Button(self, text=msg)

title.config(bg=’steelblue’, fg=’white’, relief=RIDGE)

title.config(command=self.onShowHelp)

title.pack(fill=X)

def onShowHelp(self):

загружает и отображает блок текстовых строк

3.0: теперь использует также HTML и модуль webbrowser

выбор между текстом, HTML или отображением и в том, и в другом формате определяется настройками в mailconfig всегда отображает справку: если оба параметра имеют значение, отображается справка в формате html

if mailconfig.showHelpAsText:

from PyMailGuiHelp import helptext

popuputil.HelpPopup(appname, helptext,

showsource=self.onShowMySource)

if mailconfig.showHelpAsHTML or (not mailconfig.showHelpAsText):

from PyMailGuiHelp import showHtmlHelp

showHtmlHelp() # 3.0: HTMLверсия не предусматривает возможность # вывести файлы с исходными текстами

def onShowMySource(self, showAsMail=False):

отображает файлы с исходными текстами плюс импортирует модули кое-где

import PyMailGui, ListWindows, ViewWindows, SharedNames, textConfig

from PP4E.Internet.Email.mailtools import ( # mailtools теперь пакет mailSender, mailFetcher, mailParser) # невозм. использовать *

mymods = ( # в инструкции def

PyMailGui, ListWindows, ViewWindows, SharedNames,

PyMailGuiHelp, popuputil, messagecache, wraplines, html2text, mailtools, mailFetcher, mailSender, mailParser,

mailconfig, textConfig, threadtools, windows, textEditor) for mod in mymods:

source = mod.__file__

if source.endswith(‘.pyc’):

source = source[:-4] + ‘.py# предполагается присутствие

if showAsMail: # файлов .py в том же каталоге

#   не очень элегантно…

code = open(source).read() # 3.0: кодировка для платформы

user = mailconfig.myaddress

hdrmap = {‘From’: appname, ‘To’: user, ‘Subject’: mod.__name__}

ViewWindow(showtext=code,

headermap=hdrmap, origmessage=email.message.Message()) else:

#   более удобный текстовый редактор PyEdit

#   4E: предполагается, что текст в кодировке UTF8

#   (иначе PyEdit может запросить кодировку!) wintitle = ‘ — ‘ + mod.__name__ textEditor.TextEditorMainPopup(self, source, wintitle, ‘utf-8’)

def onLoadServer(self, forceReload=False):

может вызываться в потоках: загружает или повторно загружает список заголовков почты по требованию; операции Exit,Fail,Progress вызываются методом threadChecker посредством очереди в обработчике after; загрузка может перекрываться

во времени с отправкой, но запрещает все остальные операции;

можно было бы выполнять одновременно с oadingMsgs,

но может изменять кэш сообщений; в случае ошибки операций удаления/синхронизации принудительно вызывается forceReload, иначе загружает только заголовки вновь прибывших сообщений;

2.1:  cache.loadHeaders можно использовать для быстрой проверки синхронизации номеров сообщений с сервером, когда загружаются только заголовки вновь прибывших сообщений;

if loadingHdrsBusy or deletingBusy or loadingMsgsBusy:

showerror(appname, ‘Cannot load headers during load or delete’) else:

loadingHdrsBusy.incr()

self.cache.setPopPassword(appname) # не обновлять графический

# интерфейс в потоке!

popup = popuputil.BusyBoxNowait(appname,

‘Loading message headers’)

threadtools.startThread(

action = self.cache.loadHeaders,

args = (forceReload,),

context = (popup,),

onExit = self.onLoadHdrsExit,

onFail = self.onLoadHdrsFail,

onProgress = self.onLoadHdrsProgress)

def onLoadHdrsExit(self, popup):

self.fillIndex()

popup.quit()

self.lift()

loadingHdrsBusy.decr() # разрешить выполнение других операций

def onLoadHdrsFail(self, exc_info, popup):

popup.quit()

showerror(appname, ‘Load failed: \n%s\n%s’ % exc_info[:2])

printStack(exc_info) # вывести трассировку стека в stdout

loadingHdrsBusy.decr()

if exc_info[0] == mailtools.MessageSynchError: # синхронизировать self.onLoadServer(forceReload=True) # нов. поток: перезагр.

else:

self.cache.popPassword = None # заставить ввести в следующий раз

def onLoadHdrsProgress(self, i, n, popup):

popup.changeText(‘%d of %d’ % (i, n))

def doDelete(self, msgnumlist):

может вызываться в потоках: теперь удаляет почту на сервере — изменяет номера сообщений;

может перекрываться во времени только с операцией отправки, запрещает все операции, кроме отправки;

2.1: cache.deleteMessages теперь проверяет результат команды TOP, чтобы проверить соответствие выбранных сообщений на случай рассинхронизации номеров сообщений с сервером: это возможно в случае удаления почты другим клиентом или в результате автоматического удаления сообщений сервером — в случае ошибки загрузки некоторые провайдеры могут перемещать почту из папки входящих сообщений в папку недоставленных;

if loadingHdrsBusy or deletingBusy or loadingMsgsBusy: showerror(appname, ‘Cannot delete during load or delete’) else:

deletingBusy.incr()

popup = popuputil.BusyBoxNowait(appname,

‘Deleting selected mails’)

threadtools.startThread( action = self.cache.deleteMessages,

args = (msgnumlist,),

context = (popup,),

onExit = self.onDeleteExit,

onFail = self.onDeleteFail,

onProgress = self.onDeleteProgress)

def onDeleteExit(self, popup):

self.fillIndex() # не требуется повторно загружать с сервера popup.quit() # заполнить оглавление обновленными данными из кэша self.lift() # поднять окно с оглавлением, освободить блокировку deletingBusy.decr()

def onDeleteFail(self, exc_info, popup):

popup.quit()

showerror(appname, ‘Delete failed: \n%s\n%s’ % exc_info[:2])

printStack(exc_info) # ошибка удаления

deletingBusy.decr() # или проверки синхронизации

self.onLoadServer(forceReload=True) # новый поток: номера изменились

def onDeleteProgress(self, i, n, popup):

popup.changeText(‘%d of %d’ % (i, n))

def getMessages(self, msgnums, after):

может вызываться в потоках: загружает все выбранные сообщения в кэш; используется операциями сохранения, просмотра, создания ответа и пересылки для предварительного заполнения кэша; может перекрываться во времени с другими операциями загрузки сообщений и отправки, запрещает выполнение операции удаления

и загрузки заголовков; действие "after" выполняется, только если получение сообщений разрешено и было выполнено успешно;

2.1: cache.getMessages проверяет синхронизацию оглавления с сервером,но здесь проверка выполняется только при необходимости взаимодействия с сервером, когда требуемые сообщения отсутствуют в кэше;

3.0: смотрите примечания к messagecache: теперь предотвращаются попытки загрузить сообщения, которые в настоящий момент уже загружаются, если пользователь щелкнет мышью, чтобы повторно запустить операцию, когда она уже выполняется;

если какое-либо сообщение из числа выбранных уже загружается другим запросом, необходимо запретить загрузку всего пакета сообщений toLoad: иначе необходимо ждать завершения N других операций загрузки; операции загрузки по-прежнему могут перекрываться во времени fetches при условии, что их ничто не объединяет;

if loadingHdrsBusy or deletingBusy:

showerror(appname, ‘Cannot fetch message during load or delete’) else:

toLoad = [num for num in msgnums if not self.cache.isLoaded(num)] if not toLoad:

after() # все уже загружено

return # обработать сейчас, не ждать диалога

else:

if set(toLoad) & self.beingFetched: # 3.0: хоть одно загруж.? showerror(appname,

‘Cannot fetch any message being fetched’)

else

self.beingFetched |= set(toLoad)

loadingMsgsBusy.incr()

from popuputil import BusyBoxNowait

popup = BusyBoxNowait(appname,

‘Fetching message contents’)

threadtools.startThread( action = self.cache.getMessages,

args = (toLoad,),

context = (after, popup, toLoad),

onExit = self.onLoadMsgsExit,

onFail = self.onLoadMsgsFail,

onProgress = self.onLoadMsgsProgress)

def onLoadMsgsExit(self, after, popup, toLoad):

self.beingFetched -= set(toLoad)

popup.quit()

after()

loadingMsgsBusy.decr() # разрешить другие операции после onExit

def onLoadMsgsFail(self, exc_info, after, popup, toLoad):

self.beingFetched -= set(toLoad)

popup.quit()

showerror(appname, ‘Fetch failed: \n%s\n%s’ % exc_info[:2])

printStack(exc_info)

loadingMsgsBusy.decr()

if exc_info[0] == mailtools.MessageSynchError: # синхр. с сервером self.onLoadServer(forceReload=True) # новый поток: перезагр.

def onLoadMsgsProgress(self, i, n, after, popup, toLoad): popup.changeText(‘%d of %d’ % (i, n))

def getMessage(self, msgnum):

return self.cache.getMessage(msgnum) # полный текст

def headersMaps(self):

#   список объектов email.message.Message, в 3.x требуется вызвать

#   функцию list() при использовании map()

#   возвращает [self.parseHeaders(h) for h in self.cache.allHdrs()]

return list(map(self.parseHeaders, self.cache.allHdrs()))

def mailSize(self, msgnum):

return self.cache.getSize(msgnum)

def okayToQuit(self):

#   есть хоть один действующий поток?

filesbusy = [win for win in openSaveFiles.values()

if win.openFileBusy]

busy = loadingHdrsBusy or deletingBusy or \

sendingBusy or loadingMsgsBusy

busy = busy or filesbusy

return not busy

ViewWindows: окна просмотра сообщений

В примере 14.4 приводится реализация окон просмотра и редактирования почтовых сообщений. Эти окна создаются в ответ на выполнение действий, запускаемых в окнах со списками кнопками View (Просмотреть), Write (Написать), Reply (Ответить) и Forward (Переслать). Порядок создания этих окон смотрите в обработчиках событий этих кнопок в модуле с реализацией окон со списками, представленном в примере 14.3.

Как и предыдущий модуль (пример 14.3), этот файл в действительности содержит один общий класс и несколько его расширений. Окно просмотра почтового сообщения практически идентично окну редактирования сообщения, используемому для выполнения операций Write (Написать), Reply (Ответить) и Forward (Переслать). В результате этот пример определяет общий суперкласс окна просмотра, реализующий внешний вид и поведение, и затем расширяет его подклассом окна редактирования.

Окна для создания ответа и пересылки здесь практически не отличаются от окна создания нового почтового сообщения, потому что все отличия (например, установка заголовков «From» и «To», вставка цитируемого текста) обрабатываются в реализации окна со списком, еще до того как будет создано окно редактирования.

Пример 14.4. PP4E\Internet\Email\PyMailGui\ViewWindows.py

############################################################################ Реализация окон просмотра сообщения, создания нового, ответа и пересылки: каждому типу соответствует свой класс. Здесь применяется прием выделения общего программного кода для многократного использования: класс окна создания нового сообщения является подклассом окна просмотра, а окна ответа и пересылки являются подклассами окна создания нового сообщения.

Окна, определяемые в этом файле, создаются окнами со списками, в ответ на действия пользователя.

Предупреждение: действие кнопки Split‘, открывающей части/вложения, недостаточно очевидно. 2.1: эта проблема была устранена добавлением кнопок быстрого доступа к вложениям.

Новое в версии 3.0: для размещения полей заголовков вместо фреймов колонок задействован менеджер компоновки grid(), одинаково хорошо действующий на всех платформах.

Новое в версии 3.0: добавлена поддержка кодировок Юникода для основного текста сообщения + текстовых вложений при передаче.

Новое в версии 3.0: PyEdit поддерживает произвольные кодировки Юникода для просматриваемых частей сообщений.

Новое в версии 3.0: реализована поддержка кодировок Юникода и форматов MIME для заголовков в отправляемых сообщениях.

Что сделать: можно было бы не выводить запрос перед закрытием окна, если текст сообщения не изменялся (как в PyEdit2.0), но эти окна несколько больше, чем просто редактор, и не определяют факт изменения заголовков.

Что сделать: возможно, следует сделать диалог выбора файла в окнах создания новых сообщений глобальным для всей программы? (сейчас каждое окно создает собственный диалог).

############################################################################

from SharedNames import * # объекты, глобальные для всей программы

############################################################################ # окно просмотра сообщения — также является суперклассом для окон создания # нового сообщения, ответа и пересылки

############################################################################

class ViewWindow(windows.PopupWindow, mailtools.MailParser):

подкласс класса Toplevel с дополнительными методами и встроенным

объектом TextEditor; наследует saveParts, partsList

из mailtools.MailParser; подмешивается в логику специализированных подклассов прямым наследованием;

# атрибуты класса

modelabel = View # используется в заголовках окон

from mailconfig import okayToOpenParts # открывать ли вложения?

from mailconfig import verifyPartOpens # выводить запрос перед

# открытием каждой части?

from mailconfig import maxPartButtons # макс. число кнопок + ‘…’

from mailconfig import skipTextOnHtmlPart # 3.0: только в броузере,

#   не использовать PyEdit?

tempPartDir = ‘TempParts’ # каталог для временного

#   сохранения вложений

#   все окна просмотра используют один и тот же диалог: запоминается

#   последний каталог

partsDialog = Directory(title=appname + ‘: Select parts save directory’)

def __init__(self, headermap, showtext, origmessage=None):

карта заголовков — это origmessage или собственный словарь с заголовками (для операции создания нового письма);

showtext — основная текстовая часть сообщения: извлеченная из сообщения или любой другой текст;

origmessage объект email.message.Message для просмотра windows.PopupWindow.__init__(self, appname, self.modelabel) self.origMessage = origmessage self.makeWidgets(headermap, showtext)

def makeWidgets(self, headermap, showtext):

добавляет поля заголовков, кнопки операций и доступа к вложениям, текстовый редактор

3.0: предполагается, что в аргументе showtext передается декодированная строка Юникода str; она будет кодироваться при отправке или при сохранении;

actionsframe = self.makeHeaders(headermap)

if self.origMessage and self.okayToOpenParts:

self.makePartButtons()

self.editor = textEditor.TextEditorComponentMinimal(self)

myactions = self.actionButtons() for (label, callback) in myactions:

b = Button(actionsframe, text=label, command=callback) b.config(bg=’beige’, relief=RIDGE, bd=2) b.pack(side=TOP, expand=YES, fill=BOTH)

# тело текста, присоединяется последним = усекается первым self.editor.pack(side=BOTTOM) # может быть несколько редакторов

self.update() # 3.0: иначе текстовый курсор

# встанет в строке

self.editor.setAllText(showtext) # каждый имеет собств. содержимое lines = len(showtext.splitlines())

lines = min(lines + 3, mailconfig.viewheight or 20)

self.editor.setHeight(lines) # иначе высота=24, ширина=80

self.editor.setWidth(80) # или из textConfig редактора PyEdit if mailconfig.viewbg:

self.editor.setBg(mailconfig.viewbg) # цвета, шрифты в mailconfig if mailconfig.viewfg:

self.editor.setFg(mailconfig.viewfg)

if mailconfig.viewfont: # также через меню Tools редактора

self.editor.setFont(mailconfig.viewfont)

def makeHeaders(self, headermap):

добавляет поля ввода заголовков, возвращает фрейм с кнопками операций;

3.0: для создания рядов метка/поле ввода использует менеджер компоновки grid(), одинаково хорошо действующий на всех платформах; также можно было бы использовать менеджер компоновки pack() с фреймами рядов и метками фиксированной ширины;

3.0: декодирование интернационализированных заголовков

(и компонентов имен в заголовках с адресами электронной почты) выполняется здесь, если это необходимо, так как они добавляются в графический интерфейс; некоторые заголовки, возможно, уже были декодированы перед созданием окон ответа/пересылки, где требуется использовать декодированный текст, но лишнее декодирование здесь не вредит им и оно необходимо для других заголовков и случаев, таких как просмотр полученных сообщений; при отображении в графическом интерфейсе заголовки всегда находятся в декодированной форме и будут кодироваться внутри пакета mailtools при передаче, если они содержат символы за пределами диапазона ASCII (смотрите реализацию класса Write); декодирование интернационализированных заголовков также происходит в окне с оглавлением почты и при добавлении заголовков в цитируемый текст; текстовое содержимое в теле письма также декодируется перед отображением и кодируется перед передачей в другом месте в системе (окна со списками, класс WriteWindow);

3.0: при создании окна редактирования вызывающий программный код записывает адрес отправителя в заголовок Bcc, который подхватывается здесь для удобства в типичных ситуациях, если этот заголовок разрешен в mailconfig; при создании окна ответа также заполняется заголовок Cc, если разрешен, уникальными адресами получателей оригинального письма, включая адрес в заголовке From;

top = Frame(self); top.pack (side=TOP, fill=X)

left = Frame(top); left.pack (side=LEFT, expand=NO, fill=BOTH)

middle = Frame(top); middle.pack(side=LEFT, expand=YES, fill=X)

# множество заголовков может быть расширено в mailconfig (Bcc и др.?) self.userHdrs = ()

showhdrs = (‘From’, ‘To’, ‘Cc’, ‘Subject’)

if hasattr(mailconfig, ‘viewheaders’) and mailconfig.viewheaders: self.userHdrs = mailconfig.viewheaders showhdrs += self.userHdrs

addrhdrs = (‘From’, ‘To’, ‘Cc’, ‘Bcc’) # 3.0: декодируются отдельно self.hdrFields = []

for (i, header) in enumerate(showhdrs):

lab = Label(middle, text=header+’:’, justify=LEFT)

ent = Entry(middle)

lab.grid(row=i, column=0, sticky=EW)

ent.grid(row=i, column=1, sticky=EW)

middle.rowconfigure(i, weight=1)

hdrvalue = headermap.get(header, ‘?’) # может быть пустым

# 3.0: если закодирован, декодировать с учетом email+mime+юникод if header not in addrhdrs:

hdrvalue = self.decodeHeader(hdrvalue)

else:

hdrvalue = self.decodeAddrHeader(hdrvalue)

ent.insert(‘0’, hdrvalue)

self.hdrFields.append(ent) # порядок имеет значение в onSend

middle.columnconfigure(1, weight=1)

return left

def actionButtons(self): # должны быть методами для доступа к self

return [(‘Cancel’, self.destroy), # закрыть окно просмотра тихо

(‘Parts‘, self.onParts), # список частей или тело

(‘Split’, self.onSplit)]

def makePartButtons(self):

добавляет до N кнопок быстрого доступа к частям/вложениям; альтернатива кнопкам Parts/Split (2.1); это нормально, когда временный каталог совместно используется всеми операциями файл вложения не сохраняется, пока позднее не будет выбран и открыт; partname=partname требуется в lambda-выражениях в Py2.4;

предупреждение: можно было бы попробовать пропустить главную текстовую часть;

def makeButton(parent, text, callback):

link = Button(parent, text=text, command=callback, relief=SUNKEN) if mailconfig.partfg: link.config(fg=mailconfig.partfg) if mailconfig.partbg: link.config(bg=mailconfig.partbg) link.pack(side=LEFT, fill=X, expand=YES)

parts = Frame(self)

parts.pack(side=TOP, expand=NO, fill=X)

for (count, partname) in enumerate(self.partsList(self.origMessage)): if count == self.maxPartButtons:

makeButton(parts, ‘…’, self.onSplit) break

openpart = (lambda partname=partname: self.onOnePart(partname)) makeButton(parts, partname, openpart)

def onOnePart(self, partname):

отыскивает часть, соответствующую кнопке, сохраняет ее и открывает; допускается открывать несколько сообщений: каждый раз сохранение производится заново; вероятно, мы могли бы здесь просто использовать веб-броузер;

предупреждение: tempPartDir содержит путь относительно cwdможет быть любым каталогом;

предупреждение: tempPartDir никогда не очищается: может занимать много места на диске, можно было бы использовать модуль tempfile (как при отображении главной текстовой части в формате HTML в методе onView класса окна со списком);

try:

savedir = self.tempPartDir

message = self.origMessage

(contype, savepath) = self.saveOnePart(savedir, partname, message) except:

showerror(appname, ‘Error while writing part file’)

printStack(sys.exc_info())

else:

self.openParts([(contype,

os.path.abspath(savepath))]) # повт. исп.

def onParts(self):

отображает содержимое части/вложения сообщения в отдельном окне; использует ту же схему именования файлов, что и операция Split; если сообщение содержит единственную часть, она является текстовым телом

partnames = self.partsList(self.origMessage)

msg = ‘\n’.join([‘Message parts:\n’] + partnames) showinfo(appname, msg)

def onSplit(self):

выводит диалог выбора каталога и сохраняет туда все части/вложения; при желании мультимедийные части и HTML открываются в веб-броузере, текст — в TextEditor, а документы известных типов — в соответствующих программах Windows;

можно было бы отображать части в окнах View, где имеется встроенный текстовый редактор с функцией сохранения, но большинство частей являются нечитаемым текстом;

savedir = self.partsDialog.show() # атрибут класса: предыдущий каталог if savedir: # диалог tk выбора каталога, не файла

try:

partfiles = self.saveParts(savedir, self.origMessage) except:

showerror(appname, ‘Error while writing part files’) printStack(sys.exc_info()) else:

if self.okayToOpenParts: self.openParts(partfiles)

def askOpen(self, appname, prompt):

if not self.verifyPartOpens: return True

else:

return askyesno(appname, prompt) # диалог

def openParts(self, partfiles):

автоматически открывает файлы известных и безопасных типов, но только после подтверждения пользователем; файлы других типов должны открываться вручную из каталога сохранения;

к моменту вызова этого метода именованные части уже преобразованы из формата MIME и хранятся в двоичных файлах, однако текстовые файлы могут содержать текст в любой кодировке; редактору PyEdit требуется знать кодировку для декодирования, веб-броузеры могут пытаться сами определять кодировку или позволять сообщать ее им;

предупреждение: не открывает вложения типа application/octetstream, даже если имя файла имеет безопасное расширение, такое как .html; предупреждение: изображения/аудио/видео можно было бы открывать с помощью сценария playfile.py из этой книги; в случае ошибки средства просмотра текста: в Windows можно было бы также запускать Notepad Блокнот) с помощью startfile;

(в большинстве случаев также можно было бы использовать модуль webbrowser, однако специализированный инструмент всегда лучше универсального;

def textPartEncoding(fullfilename)

3.0: отображает имя файла текстовой части в содержимое параметра charset в заголовке contenttype для данной части сообщения Message, которое затем передается конструктору PyEdit, чтобы обеспечить корректное отображение текста; для текстовых частей можно было бы возвращать параметр charset вместе с contenttype из mailtools, однако проще обрабатывать эту ситуацию как особый случай здесь;

содержимое части сохраняется пакетом mailtools в двоичном режиме, чтобы избежать проблем с кодировками, но здесь отсутствует прямой доступ к оригинальной части сообщения; необходимо выполнить это отображение, чтобы получить имя кодировки, если оно присутствует;

редактор PyEdit в 4 издании теперь позволяет явно указывать кодировку открываемого файла и определяет кодировку при сохранении; смотрите главу 11, где описываются особенности поведения PyEdit: он запрашивает кодировку у пользователя, если кодировка не указана или оказывается неприменимой; предупреждение: перейти на mailtools.mailParser в PyMailCGI, чтобы повторно использовать для тега <meta>?

partname = os.path.basename(fullfilename)

for (filename, contype, part) in \

self.walkNamedParts(self.origMessage):

if filename == partname:

return part.get_content_charset() # None, если нет # в заг.

assert False, ‘Text part not found’ # никогда не должна # выполняться

for (contype, fullfilename) in partfiles:

maintype = contype.split(‘/’)[0] # левая часть

extension = os.path.splitext(fullfilename)[1] # не [-4:] basename = os.path.basename(fullfilename) # отбросить путь

#  текст в формате HTML и XML, веб-страницы, некоторые

#  мультимедийные файлы

if contype in [‘text/html’, ‘text/xml’]:

browserOpened = False

if self.askOpen(appname, ‘Open "%s" in browser?’ % basename): try:

webbrowser.open_new(‘file://’ + fullfilename) browserOpened = True

except:

showerror(appname, ‘Browser failed: trying editor’)

if not browserOpened or not self.skipTextOnHtmlPart: try:

# попробовать передать редактору PyEdit имя кодировки encoding = textPartEncoding(fullfilename)

textEditor.TextEditorMainPopup(parent=self, winTitle=’ %s email part’ % (encoding or ‘?’), loadFirst=fullfilename, loadEncode=encoding) except:

showerror(appname, ‘Error opening text viewer’)

#   text/plain, text/x-python и др.; 4E: кодировка может не подойти elif maintype == ‘text’:

if self.askOpen(appname, ‘Open text part "%s"?’ % basename): try:

encoding = textPartEncoding(fullfilename)

textEditor.TextEditorMainPopup(parent=self,

winTitle=’ %s email part’ % (encoding or ‘?’), loadFirst=fullfilename, loadEncode=encoding)

except:

showerror(appname, ‘Error opening text viewer’)

#   мультимедийные файлы: Windows открывает # mediaplayer, imageviewer и так далее elif maintype in [‘image’, ‘audio’, ‘video’]:

if self.askOpen(appname, ‘Open media part "%s"?’ % basename): try:

webbrowser.open_new(‘file://’ + fullfilename) except:

showerror(appname, ‘Error opening browser’)

#   типичные документы Windows: Word, Excel, Adobe, архивы и др. elif (sys.platform[:3] == ‘win’ and

maintype == ‘application’ and # 3.0: +x типы

extension in [‘.doc’, ‘.docx’, ‘.xls’, ‘.xlsx’,# обобщить

‘.pdf’, ‘.zip’, ‘.tar’, ‘.wmv’]):

if self.askOpen(appname, ‘Open part "%s"?’ % basename): os.startfile(fullfilename)

else: # пропустить!

msg = ‘Cannot open part: "%s"\nOpen manually in: "%s"’ msg = msg % (basename, os.path.dirname(fullfilename)) showinfo(appname, msg)

############################################################################ # окна редактирования сообщений — операции создания нового сообщения, # ответа и пересылки

############################################################################

if mailconfig.smtpuser: # пользователь определен в mailconfig?

MailSenderClass = mailtools.MailSenderAuth # требуется имя/пароль else:

MailSenderClass = mailtools.MailSender

class WriteWindow(ViewWindow, MailSenderClass):

специализирует окно просмотра для составления нового сообщения

наследует sendMessage из mailtools.MailSender modelabel = ‘Write’

def __init__(self, headermap, starttext):

ViewWindow.__init__(self, headermap, starttext)

MailSenderClass.__init__(self)

self.attaches = [] # каждое окно имеет свой диалог открытия

self.openDialog = None # диалог запоминает последний каталог

def actionButtons(self): # должны быть методами для доступа к self

return [(‘Cancel’, self.quit),

(‘Parts’, self.onParts), # PopupWindow проверяет отмену

(‘Attach’, self.onAttach),

(‘Send’, self.onSend)] # 4E: без отступов: по центру

def onParts(self):

# предупреждение: удаление в настоящее время не поддерживается

if not self.attaches:

showinfo(appname, ‘Nothing attached’)

else:

msg = ‘\n’.join([‘Already attached:\n’] + self.attaches) showinfo(appname, msg)

def onAttach(self):

вкладывает файл в письмо: имя, добавляемое здесь, будет добавлено как часть в операции Send, внутри пакета mailtools;

4E: имя кодировки Юникода можно было бы запрашивать здесь, а не при отправке if not self.openDialog:

self.openDialog = Open(title=appname + ‘: Select Attachment File’) filename = self.openDialog.show() # запомнить каталог if filename:

self.attaches.append(filename) # для открытия в методе отправки

def resolveUnicodeEncodings(self):

3.0/4E: в качестве подготовки к отправке определяет кодировку Юникода для текстовых частей: для основной текстовой части и для любых текстовых вложений; кодировка для основной текстовой части может быть уже известна, если это ответ или пересылка, но она не известна при создании нового письма, к тому же в результате редактирования кодировка может измениться; модуль smtplib в 3.1 требует, чтобы полный текст отправляемого сообщения содержал только символы ASCII (если это str), поэтому так важно определить кодировку прямо здесь; иначе будет возникать ошибка при попытке отправить ответ/пересылаемое письмо с текстом в кодировке UTF8, когда установлен параметр config=ascii, а текст содержит символы вне диапазона ASCII; пытается использовать настройки пользователя и выполнить ответ, а в случае неудачи возвращается к универсальной кодировке UTF8 как к последней возможности;

def isTextKind(filename):

contype, encoding = mimetypes.guess_type(filename)

if contype is None or encoding is not None: # утилита 4E return False # не определяется, сжатый файл?

maintype, subtype = contype.split(‘/’, 1) # проверить на text/? return maintype == ‘text’

#   выяснить кодировку основного текста

bodytextEncoding = mailconfig.mainTextEncoding

if bodytextEncoding == None:

asknow = askstring(‘PyMailGUI’,

‘Enter main text Unicode encoding name’)

bodytextEncoding = asknow or ‘latin-1’ # или

# sys.getdefaultencoding()?

#   последний шанс: использовать utf-8, если кодировку

#   так и не удалось определить выше

if bodytextEncoding != ‘utf-8’:

try:

bodytext = self.editor.getAllText()

bodytext.encode(bodytextEncoding)

except (UnicodeError, LookupError): # Lookup: неверная кодировка bodytextEncoding = ‘utf-8’ # универсальная кодировка

#   определить кодировки текстовых вложений

attachesEncodings = []

config = mailconfig.attachmentTextEncoding

for filename in self.attaches:

if not isTextKind(filename):

attachesEncodings.append(None) # не текст: не спрашивать

elif config != None:

attachesEncodings.append(config) # для всех текстовых

else: # частей, если установлена

prompt = ‘Enter Unicode encoding name for %’ % filename asknow = askstring(‘PyMailGUI’, prompt)

attachesEncodings.append(asknow or ‘latin-1’)

#   последний шанс: использовать utf-8, если кодировку

#   так и не удалось определить выше

choice = attachesEncodings[-1]

if choice != None and choice != ‘utf-8’:

try:

attachbytes = open(filename, ‘rb’).read() attachbytes.decode(choice)

except (UnicodeError, LookupError, IOError):

attachesEncodings[-1] = ‘utf-8’

return bodytextEncoding, attachesEncodings

def onSend(self)

может вызываться из потока: обработчик кнопки

Send (Отправить) в окне редактирования;

может перекрываться во времени с любыми другими потоками выполнения, не запрещает никаких операций, кроме завершения; обработчики Exit, Fail выполняются методом threadChecker посредством очереди в обработчике after;

предупреждение: здесь не предусматривается вывод информации о ходе выполнения, потому что операция отправки почты является атомарной; допускается указывать несколько адресов получателей, разделенных ‘,’;

пакет mailtools решает проблемы с кодировками, обрабатывает вложения, форматирует строку даты и так далее; кроме того, пакет mailtools сохраняет текст отправленных сообщений в локальном файле

3.0: теперь выполняется полный разбор заголовков To,Cc,Bcc

mailtools) вместо простого разбиения по символу-разделителю; вместо простых полей ввода можно было бы использовать многострочные виджеты;

содержимое заголовка Bcc добавляется на "конверт", а сам заголовок удаляется;

3.0: кодировка Юникода для текстовых частей определяется здесь, потому что она может потребоваться для вывода подсказок в графическом интерфейсе; фактическое кодирование текстовых частей выполняется внутри пакета mailtools, если это необходимо;

3.0: интернационализированные заголовки уже декодируются в полях ввода графического интерфейса; кодирование любых интернационализированных заголовков с символами вне диапазона ASCII выполняется не здесь, а в пакете mailtools, потому что эта операция не требуется для работы графического интерфейса;

#  определить кодировку для текстовых частей;

bodytextEncoding, attachesEncodings = self.resolveUnicodeEncodings()

#  получить компоненты графического интерфейса;

#  3.0: интернационализированные заголовки уже декодированы fieldvalues = [entry.get() for entry in self.hdrFields] From, To, Cc, Subj = fieldvalues[:4]

extraHdrs = [(‘Cc’, Cc), (‘X-Mailer’, appname + ‘ (Python)’)] extraHdrs += list(zip(self.userHdrs, fieldvalues[4:])) bodytext = self.editor.getAllText()

#  разбить список получателей на адреса по ‘,’, исправить пустые поля

Tos = self.splitAddresses(To)

for (ix, (name, value)) in enumerate(extraHdrs):

if value: # игнорировать, если »

if value == ‘?’: # ? не заменяется

extraHdrs[ix] = (name, »)

elif name.lower() in [‘cc’, ‘bcc’]: # разбить по ‘,’ extraHdrs[ix] = (name, self.splitAddresses(value))

#   метод withdraw вызывается, чтобы предотвратить повторный запуск

#   передачи во время передачи

#   предупреждение: не устраняет вероятность ошибки полностью —

#   пользователь может восстановить окно, если значок

#   останется видимым

self.withdraw()

self.getPassword() # если необходимо; не запускайте диалог в потоке! popup = popuputil.BusyBoxNowait(appname, ‘Sending message’) sendingBusy.incr()

threadtools.startThread(

action = self.sendMessage,

args = (From, Tos, Subj, extraHdrs, bodytext, self.attaches, saveMailSeparator, bodytextEncoding, attachesEncodings),

context = (popup,),

onExit = self.onSendExit,

onFail = self.onSendFail)

def onSendExit(self, popup):

стирает окно ожидания, стирает окно просмотра, уменьшает счетчик операций отправки; метод sendMessage автоматически сохраняет отправленное сообщение в локальном файле; нельзя использовать window.addSavedMails: текст почтового сообщения недоступен;

popup.quit()

self.destroy()

sendingBusy.decr()

#   может быть \ при открытии, в mailconfig используется /

sentname = os.path.abspath(mailconfig.sentmailfile) # расширяет ‘.’ if sentname in openSaveFiles.keys(): # файл открыт?

window = openSaveFiles[sentname] # обновить список

window.loadMailFileThread() # и поднять окно

def onSendFail(self, exc_info, popup):

#   выводит диалог с сообщением об ошибке, оставляет нетронутым окно

#   с сообщением, чтобы имелась возможность сохранить или повторить

#   попытку, перерисовывает фрейм

popup.quit()

self.deiconify()

self.lift()

showerror(appname, ‘Send failed: \n%s\n%s’ % exc_info[:2])

printStack(exc_info)

MailSenderClass.smtpPassword = None # повт. попытку; 3.0/4E: не в self sendingBusy.decr()

def askSmtpPassword(self):

получает пароль, если он необходим графическому интерфейсу, вызывается из главного потока выполнения;

предупреждение: чтобы не возникла необходимость запрашивать пароль в потоке выполнения, если он не был введен в первый раз, выполняет цикл, пока пользователь не введет пароль; смотрите логику получения пароля доступа к POP-серверу, где приводится альтернативный вариант без цикла password = »

while not password:

prompt = (‘Password for %s on %s?’ %

(self.smtpUser, self.smtpServerName))

password = popuputil.askPasswordWindow(appname, prompt) return password

class ReplyWindow(WriteWindow)

специализированная версия окна создания сообщения для ответа текст и заголовки поставляются окном со списком modelabel = Reply

class ForwardWindow(WriteWindow)

подпись: сообщения для пересылкиспециализированная версия окна создания текст и заголовки поставляются окном со списком modelabel = Forward

messagecache: менеджер кэша сообщений

Класс в примере 14.5 реализует кэш для хранения загруженных сообщений. Его логика была выделена в отдельный файл, чтобы не загромождать реализацию окон со списками. Окно со списком сообщений на сервере создает и встраивает экземпляр этого класса для обеспечения взаимодействий с почтовым сервером и сохранения загруженных заголовков и полного текста сообщений. В этой версии окно со списком сообщений на сервере также запоминает, какие письма загружаются в текущий момент, чтобы избежать попытки загрузить одно и то же письмо несколько раз в параллельных потоках. Данная задача не была реализована здесь лишь потому, что она может потребовать операций с графическим интерфейсом.

Пример 14.5. PP4E\Internet\Email\PyMailGui\messagecache.py

############################################################################ управляет загрузкой сообщений с заголовками и контекстом, но не графическим интерфейсом; подкласс класса MailFetcher, со списком загруженных заголовков и сообщений; вызывающая программа сама должна заботиться о поддержке потоков выполнения и графического интерфейса;

изменения в версии 3.0: использует кодировку для полного текста сообщений из локального модуля mailconfig; декодирование выполняется глубоко в недрах mailtools, после загрузки текст сообщения всегда возвращается в виде строки Юникода str; это может измениться в будущих версиях Python/email: подробности смотрите в главе 13;

изменения в версии 3.0: поддерживает новую особенность mailconfig.fetchlimit в mailtools, которая может использоваться для ограничения максимального числа самых свежих заголовков или сообщений (если не поддерживается команда TOP), загружаемых при каждом запросе на загрузку; обратите внимание, что эта особенность является независимой от параметра loadfrom, используемого здесь, чтобы ограничить загрузку только самыми новыми сообщениями, хотя они и используются одновременно: загружается не больше чем fetchlimit вновь поступивших сообщений;

изменения в версии 3.0: есть вероятность, что пользователь запросит загрузку сообщения, которое в текущий момент уже загружается в параллельном потоке, просто щелкнув на сообщении еще раз (операции загрузки сообщений, в отличие от полной загрузки оглавления, могут перекрываться во времени с другими операциями загрузки и отправки); в этом нет никакой опасности, но это может привести к излишней и, возможно, параллельной загрузке одного и того же письма, что бессмысленно и ненужно (если выбрать все сообщения в списке и дважды нажать кнопку View, это может вызвать загрузку большинства сообщений дважды!); в главном потоке графического интерфейса слежение за загружаемыми сообщениями, чтобы такое перекрытие во времени не было возможным: загружаемое сообщение препятствует выполнению операций загрузки любых наборов сообщений, в которых оно присутствует, параллельная загрузка непересекающихся множеств сообщений по-прежнему возможна;

############################################################################ from PP4E.Internet.Email import mailtools from popuputil import askPasswordWindow

class MessageInfo:

элемент списка в кэше def __init__(self, hdrtext, size):

self.hdrtext = hdrtext # fulltext кэшированное сообщение self.fullsize = size # hdrtext только заголовки

self.fulltext = None # fulltext=hdrtext если не работает

# команда TOP

class MessageCache(mailtools.MailFetcher)

следит за уже загруженными заголовками и сообщениями;

наследует от MailFetcher методы взаимодействия с сервером;

может использоваться в других приложениях: ничего не знает о графическом интерфейсе или поддержке многопоточной модели выполнения;

3.0: байты исходного полного текста сообщения декодируются в str, чтобы обеспечить возможность анализа пакетом email в Py3.1 и сохранения в файлах; использует настройки определения кодировок из локального модуля mailconfig; декодирование выполняется автоматически в пакете mailtools при получении;

def __init__(self):

mailtools.MailFetcher.__init__(self) # 3.0: наследует fetchEncoding

self.msglist = [] # 3.0: наследует fetchlimit

def loadHeaders(self, forceReloads, progress=None):

здесь обрабатываются три случая: первоначальная загрузка всего списка, загрузка вновь поступившей почты и принудительная перезагрузка после удаления; не получает повторно просмотренные сообщения, если список заголовков остался прежним или был дополнен; сохраняет кэшированные сообщения после удаления, если операция удаления завершилась успешно;

2.1:  выполняет быструю проверку синхронизации номеров сообщений 3.0: теперь учитывает максимум mailconfig.fetchlimit;

if forceReloads: loadfrom = 1 self.msglist = [] # номера сообщений изменились

else:

loadfrom = len(self.msglist)+1 # продолжить с места посл. загрузки

#  только если загружается вновь поступившая почта if loadfrom != 1:

self.checkSynchError(self.allHdrs()) # возб. искл. при рассинхр.

#  получить все или только новые сообщения

reply = self.downloadAllHeaders(progress, loadfrom)

headersList, msgSizes, loadedFull = reply

for (hdrs, size) in zip(headersList, msgSizes):

newmsg = MessageInfo(hdrs, size)

if loadedFull: # zip может вернуть пустой результат

newmsg.fulltext = hdrs # получить полные сообщения, если

#  elf.msglist.append(newmsg) # не поддерживается команда TOP

def getMessage(self, msgnum): # получает исходный текст сообщения

cacheobj = self.msglist[msgnum-1] # добавляет в кэш, если получено if not cacheobj.fulltext: # безопасно использовать в потоках

fulltext = self.downloadMessage(msgnum) # 3.0: более простое cacheobj.fulltext = fulltext # кодирование

return cacheobj.fulltext

def getMessages(self, msgnums, progress=None):

получает полный текст нескольких сообщений, может вызываться в потоках выполнения;

2.1: выполняет быструю проверку синхронизации номеров сообщений; нельзя получить сообщения здесь, если не было загружено оглавление;

self.checkSynchError(self.allHdrs()) # возб. искл. при рассинхр. nummsgs = len(msgnums) # добавляет сообщения в кэш

for (ix, msgnum) in enumerate(msgnums): # некоторые возм. уже в кэше if progress: progress(ix+1, nummsgs) # подключ. только при необх. self.getMessage(msgnum) # но может выполнять подключ.

# более одного раза

def getSize(self, msgnum): # инкапсулирует структуру кэша

return self.msglist[msgnum-1].fullsize # уже изменялось однажды!

def isLoaded(self, msgnum):

return self.msglist[msgnum-1].fulltext

def allHdrs(self):

return [msg.hdrtext for msg in self.msglist]

def deleteMessages(self, msgnums, progress=None):

если удаление всех номеров сообщений возможно, изымает удаленные элементы из кэша, но не перезагружает ни список заголовков, ни текст уже просмотренных сообщений: список в кэше будет отражать изменение номеров сообщений на сервере; если удаление завершилось неудачей по каким-либо причинам, вызывающая программа должна принудительно перезагрузить все заголовки, расположенные выше, потому что номера некоторых сообщений на сервере могут измениться непредсказуемым образом;

2.1:  теперь проверяет синхронизацию номеров сообщений, если команда TOP поддерживается сервером; может вызываться в потоках выполнения try:

self.deleteMessagesSafely(msgnums, self.allHdrs(), progress)

except mailtools.TopNotSupported:

mailtools.MailFetcher.deleteMessages(self, msgnums, progress)

# ошибок не обнаружено: обновить список оглавления

indexed = enumerate(self.msglist)

self.msglist = [msg for (ix, msg) in indexed if ix+1 not in msgnums]

class GuiMessageCache(MessageCache):

вызовы графического интерфейса добавляются здесь, благодаря чему кэш можно использовать в приложениях без графического интерфейса def setPopPassword(self, appname)

получает пароль с помощью графического интерфейса, вызывается в главном потоке; принудительно вызывается из графического интерфейса, чтобы избежать вызова из дочерних потоков выполнения if not self.popPassword:

prompt = ‘Password for %s on %s?’ % (self.popUser, self.popServer) self.popPassword = askPasswordWindow(appname, prompt)

def askPopPassword(self):

но здесь не использует графический интерфейс: я вызываю его из потоков!; попытка вывести диалог в дочернем потоке выполнения подвесит графический интерфейс; может вызываться суперклассом MailFetcher, но только если пароль остается пустой строкой из-за закрытия окна диалога return self.popPassword

popuputil: диалоги общего назначения

В примере 14.6 реализовано несколько удобных вспомогательных диалогов, которые могут пригодиться в разных программах. Обратите внимание, что здесь импортируется уже знакомый нам вспомогательный модуль windows, обеспечивающий единство стиля диалогов (значки, заголовки и так далее).

Пример 14.6. PP4E\Internet\Email\PyMailGui\popuputil.py

############################################################################ вспомогательные окна — могут пригодиться в других программах

############################################################################

from tkinter import *

from PP4E.Gui.Tools.windows import PopupWindow

class HelpPopup(PopupWindow):

специализированная версия Toplevel, отображающая

справочный текст в области с прокруткой

кнопка Source вызывает указанный обработчик обратного вызова

альтернатива в версии 3.0: использовать файл HTML и модуль webbrowser myfont = system# настраивается

mywidth = 78 # 3.0: начальная ширина

def __init__(self, appname, helptext, iconfile=None, showsource=lambda:0) PopupWindow.__init__(self, appname, ‘Help’, iconfile)

from tkinter.scrolledtext import ScrolledText # немодальный диалог bar = Frame(self) # присоединся первымусекается последним

bar.pack(side=BOTTOM, fill=X)

code = Button(bar, bg=’beige’, text="Source", command=showsource)

quit = Button(bar, bg=’beige’, text="Cancel", command=self.destroy)

code.pack(pady=1, side=LEFT)

quit.pack(pady=1, side=LEFT)

text = ScrolledText(self) # добавить Text + полосы прокр.

text.config(font=self.myfont)

text.config(width=self.mywidth) # слишком большой для showinfo text.config(bg=’steelblue’, fg=’white’) # закрыть при нажатии

# на кнопку

text.insert(‘0.0’, helptext) # или на клавишу Return

text.pack(expand=YES, fill=BOTH)

self.bind("<Return>", (lambda event: self.destroy()))

def askPasswordWindow(appname, prompt):

модальный диалог для ввода строки пароля

функция getpass.getpass использует stdin, а не графический интерфейс tkSimpleDialog.askstring выводит ввод эхом

win = PopupWindow(appname, ‘Prompt’) # настроенный экземпляр Toplevel Label(win, text=prompt).pack(side=LEFT)

entvar = StringVar(win)

ent = Entry(win, textvariable=entvar, show=’*’) # показывать * ent.pack(side=RIGHT, expand=YES, fill=X)

ent.bind(‘<Return>’, lambda event: win.destroy())

ent.focus_set(); win.grab_set(); win.wait_window()

win.update() # update вызывает принудительную перерисовку

return entvar.get() # виджет ent к этому моменту уже уничтожен

class BusyBoxWait(PopupWindow):

блокирующее окно с сообщением: выполнение потока приостанавливается цикл обработки событий главного потока графического интерфейса продолжает выполняться, но сам графический интерфейс

не действует, пока открыто это окно;

используется переопределенная версия метода quit, потому что в дереве наследования он находится ниже, а не левее;

def __init__(self, appname, message):

PopupWindow.__init__(self, appname, ‘Busy’)

self.protocol(‘WM_DELETE_WINDOW’, lambda:0) # игнор. попытку закрыть label = Label(self, text=message + ‘…’) # win.quit(), чтобы закрыть label.config(height=10, width=40, cursor=’watch’) # курсор занятости label.pack()

self.makeModal() self.message, self.label = message, label

def makeModal(self):

self.focus_set() # захватить фокус ввода self.grab_set() # ждать вызова threadexit

def changeText(self, newtext):

self.label.config(text=self.message + ‘: ‘ + newtext)

def quit(self):

self.destroy() # не запрашивать подтверждение

class BusyBoxNowait(BusyBoxWait):

неблокирующее окно

вызывайте changeText, чтобы отобразить ход выполнения операции, quit — чтобы закрыть окно

def makeModal(self): pass