СЕРГЕЙ СУПРУНОВ
Практикум Python: отправка файлов по электронной почте
Как вы отправляете по электронной почте файл из Windows? Я, например, до недавнего времени делал так: находил нужный файл в дереве каталогов в FAR; набирал в командной строке «start .», чтобы открыть каталог в «Проводнике»; щелкал по файлу правой кнопкой мыши, выбирал «Add to archive…»; затем щелкал по полученному файлу архива и выбирал «Отправить –> Адресат»… В общем, долго и утомительно. Теперь я делаю это из FAR командной строкой такого вида:
C:Temp>send to me file “Годовой отчет.doc” as year2004.zip
По заголовку статьи вы уже поняли, что такого счастья удалось достичь благодаря языку Python, и здесь достаточно подробно описываются предпринятые для этого шаги. Цель статьи, как обычно, не в описании готового решения для «copy – paste», а в том, чтобы показать пути решения подобных задач.
Итак, с чем нам предстоит столкнуться. Прежде всего познакомимся с использованием модуля smtplib, входящего в стандартную библиотеку Python, и нужного нам для формирования текста почтового сообщения и собственно для отправки. Вспомним, как работать с zip-архивами. Ну и попутно решим некоторые проблемы с кодировками, традиционно присущие Windows.
Начнем с главного сценария – send.py:
Листинг 1. Начало сценария send.py
# -*- coding: cp1251 -*-
#########################################################
# Utility for send files by e-mail from command line
#--------------------------------------------------------
# Usage:
# send to <addr> files <file1[ file2...]> [(nonzipped | as <zip>)]
#########################################################
import sys, os, glob, myzip, pymaconf
from mystd import mystdin, mystdout
from pysender import pysender
В приведенном фрагменте подключаются нужные нам модули. Из них sys, os и glob входят в стандартную библиотеку Python, myzip – слегка модифицированный вариант модуля, который был разработан ранее для операций упаковки файлов (см. статью «Автоматизируем FTP с помощью Python», журнал «Системный администратор» №12, декабрь 2004 г.), а остальные будут рассмотрены в процессе работы.
Чтобы созданные наработки было удобнее использовать в дальнейшем, оформим функции нашего сценария как класс pyma. Этот класс я приведу полностью, снабжая комментариями наиболее интересные моменты. Ниже будут даны еще некоторые пояснения.
Листинг 2. Середина сценария send.py (класс pyma)
classpyma:
# функция инициализации – вводим переменные, которым нужно задать значения по умолчанию
def __init__(self):
self.FLG_NZ = False # будем ли упаковывать файлы
self.ZIPNAME = '' # имя zip-архива
# Функция разбора параметров, введенных в командной строке
def parseParameters(self, *argv):
STOP = 0
# должно быть не менее 5 параметров
iflen(argv) < 5:
self.Usage() # (имя программы + еще 4)
# 1-й обязательно «to»
if argv[1][:2] != 'to':
self.Usage()
# 3-й обязательно «fi[les]»
if argv[3][:2] != 'fi':
self.Usage()
self.TO = self.getaddrbyalias(argv[2])
# если последний параметр начинается
ifargv[-1][:2] == 'no':
# с «no», не упаковывать
self.FLG_NZ = True
STOP = -1
# если предпоследний – «as»
elif argv[-2][:2] == 'as':
# то последний – имя архива
self.ZIPNAME = argv[-1]
STOP = -2
# иначе имя архива – по 1-му файлу
else:
self.ZIPNAME = zn = argv[4]
if zn.find('*') > -1 or zn.find('?') > -1:
self.ZIPNAME = 'archive.zip'
self.ZIPNAME = Unicode(self.ZIPNAME,pymaconf.fsyscodepage).encode(pymaconf.fzipcodepage)
if self.ZIPNAME[-4:] != '.zip':
self.ZIPNAME = self.ZIPNAME + '.zip'
# все остальные параметры – шаблоны файлов
ifSTOP:
files = argv[4:STOP]
else:
files = argv[4:]
if not files:
self.Usage()
# формируем список файлов по шаблонам
self.FILELIST = []
for pattern in files:
tmplist = glob.glob(pattern)
for file in tmplist:
if os.path.isfile(file):
self.FILELIST.append(file)
if not self.FILELIST:
print '\nERROR: no files found.'
self.Usage()
# если нужно – запрос темы
if pymaconf.promptsubj:
tmp = raw_input('Subject: ')
if tmp:
pymaconf.defsubject = tmp
print
# если нужно – запрос сообщения
ifpymaconf.promptmess:
tmp = raw_input('Message: ')
if tmp:
pymaconf.defmessage = tmp
print
# Функция определения полного адреса по псевдониму
def getaddrbyalias(self, addr):
abf = os.path.dirname(__file__) + os.path.sep + 'addrbook.ab'
ab = open(abf, 'r').readlines()
aliases = {}
for line in ab:
alias, fulladdr = line.split()
aliases[alias] = fulladdr
try:
fulladdr = aliases[addr]
except:
fulladdr= addr
return fulladdr
# Функция формирования и отправки сообщения
def sendmail(self):
self.parseParameters(*sys.argv)
ifself.FLG_NZ:
# если не упаковывать – отправка файлов по списку
pysender().sendfiles(self.TO, self.FILELIST)
else:
# иначе упаковываем по списку и отправляем архив
myzip.writepattzip(self.ZIPNAME, self.FILELIST)
pysender().sendfiles(self.TO, (self.ZIPNAME,))
os.remove(self.ZIPNAME)
# Функция вывода сообщения о правильном синтаксисе
def Usage(self):
print '''
Utility for send files by e-mail from command line
Usage:
send to <addr> files <file1[ file2...]> [(nonzipped | as <zip>)]'''
sys.exit()
То есть этот модуль обеспечивает лишь разбор строки параметров, а сами функции отправки и упаковки размещены в других модулях. Обратите внимание на конструкции вида «argv[-2][:2]». Они используются для того, чтобы в параметрах командной строки значащими были только первые два символа. Операция [m:n], называемая срезом, позволяет извлечь из массива «подмассив» начиная с элемента m до элемента n, не включая последний. Если m или n пропущено, подразумевается «с начала массива» или «до конца массива» соответственно.
Текстовая строка может рассматриваться как массив одиночных символов. Благодаря этому ключ «files» можно записать и как «file», что более логично при отправке одного файла, и даже как «fi». Аналогично ключ «nonzipped» можно сокращать вплоть до «no».
Конечно, можно было бы использовать более традиционный синтаксис командной строки, но такая запись воспринимается и запоминается лучше. Сравните, например, две команды:
send–t amsand@rambler.ru –f report.txt –z rep2.zip
send to amsand@rambler.ru file report.txt as rep2.zip
Лично мне больше нравится вторая, хотя она и сложнее в разборе.
Последний отрывок файла send.py приведен ниже, но большую часть происходящего в нем я поясню немного позже. На данный момент достаточно знать, что он просто вызывает функцию отправки сообщения.
Листинг 3. Конец сценария send.py
if __name__ == '__main__':
mo = mystdout()
mi = mystdin()
mo.setmystdout()
mi.setmystdin()
nc = pyma()
nc.sendmail()
mo.setorigin()
mi.setorigin()
На этом send.py завершается. Должно быть, вы уже заметили, что нигде не видно ни адреса отправителя, ни параметров smtp-сервера, через который выполняется отправка. Конечно, все это присутствует, но вынесено в конфигурационный файл pymaconf.py, представленный ниже:
Листинг 4. Конфигурационный сценарий pymaconf.py
# -*- coding: cp1251 -*-
# Имя smtp-сервера
smtpserver = 'my.server.ru'
# Параметры smtp-авторизации
authrequire = 0
authlogin = ''
authpassword = ''
# Адрес отправителя
fromaddr = 'pyma.test@my.server.ru'
# Запрашивать ли тему сообщения
promptsubj = 1
# Тема сообщения по умолчанию
defsubject = 'Отправка файла'
# Запрашивать ли текст письма
promptmess = 1
# Текст письма по умолчанию
defmessage = 'Отправка файла\n\n\t-= pyma v.0.1 =-'
# Отправлять ли копии писем на указанный адрес
backmail = 1
backaddr = 'pyma.back@my.server.ru'
# Параметры перекодировок
basecodepage = 'cp1251' # текст в исходниках
termcodepage = 'cp866' # терминал (ввод-вывод)
mailcodepage = 'cp1251' # кодировка писем
fsyscodepage = 'cp1251' # кодировка имен файлов в системе
fzipcodepage = 'cp1251' # кодировка имени zip-архива
Собственно, здесь нет ничего интересного – просто определяется ряд переменных, которые будут использоваться в дальнейшем. Если ваш smtp-сервер требует авторизацию, установите переменную authrequire в 1 и заполните значения authlogin и authpassword. В данной программе пароль хранится в открытом виде, поэтому если доступ к вашей машине достаточно «либеральный», следует позаботиться о защите этой информации. Переменные promptsubj и promptmess определяют, должен ли выдаваться запрос на ввод темы и тела письма или следует просто подставлять значения, определенные по умолчанию.
Функция sendmail() класса pyma вызывает разбор командной строки, выделяя адрес получателя, список файлов для отправки и ключи, определяющие параметры упаковки. Функция getaddrbyalias() возвращает полный адрес по указанному в командной строке псевдониму. Если соответствие не найдено, возвращается адрес, введенный в командной строке. Сам файл addrbook.ab выглядит примерно так:
buh ivanova@my.server.ru
me amsand@rambler.ru
.. .. ..
То есть если указать «to me», почта будет отправлена на адрес amsand@rambler.ru, как если бы было указано «to amsand@rambler.ru».
Обратите внимание на конструкцию, с помощью которой определяется полный путь к файлу адресной книги:
abf = os.path.dirname(__file__) + os.path.sep + "addrbook.ab"
Поскольку нужно, чтобы утилиту отправки можно было запускать из любого каталога, и именно там Python будет искать файлы, указанные без пути, то нам нужно «привязаться» к месту размещения файла send.py. Встроенная переменная __file__ возвращает полное имя модуля, из которого она вызывается. Функция dirname() модуля os.path вырезает из полного имени файла имя каталога. Ну и чтобы обеспечить переносимость между различными операционными системами вместо явного указания слеша для отделения каталога от имени файла используем переменную os.path.sep, которая возвращает прямой слеш «/» на системах UNIX и обратный «» в Windows.
Вернемся к функции sendmail(). Если в командной строке определен ключ «nonzipped», то в процессе ее разбора функцией parseParameters() переменная FLG_NZ получит истинное значение. При этом весь список файлов, помещенный в переменную FILELIST, отправляется непосредственно в функцию sendfiles() модуля pysender (который будет рассмотрен далее). Если же требуется упаковка, что является поведением по умолчанию, то список файлов отдается на обработку функции writepattzip() модуля myzip, который был разработан ранее для графической утилиты отправки файлов по FTP. Здесь мы не будем к нему возвращаться, код этого модуля, как и всех остальных, можно будет скачать с сайта журнала http://samag.ru, в разделе «Исходный код». После упаковки функция sendfiles() получает имя созданного архива. Как только отправка завершится, zip-файл будет удален.
Вся основная работа выполняется функцией sendfiles() модуля pysender. Ниже представлен этот модуль:
Листинг 5. Модуль pysender.py
# -*- coding: cp1251 -*-
import os, smtplib, pymaconf, pymalog
class pysender:
# Функция инициализации
def __init__(self):
# Разбираем файл mime-типов.
mtf = os.path.dirname(__file__) + os.path.sep + 'mime.types'
mt = open(mtf, 'r').readlines()
self.mimetypes = {}
for line in mt:
mimetype, mimeext = line[:-1].split()
self.mimetypes[mimeext] = mimetype
# Шаблон заголовка письма, включая тело письма
messagepart = '''From: %(fromaddr)s
To: %(toaddr)s
Subject: %(subject)s
X-Mailer: PyMa 0.1
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="%(boundary)s"
This is a multi-part message in MIME format.
--%(boundary)s
Content-Type: text/plain; charset="%(codepage)s"
Content-Transfer-Encoding: Base64
%(message)s
'''
# Шаблон вложения в письме
attachpart = '''--%(boundary)s
Content-Type: %(mimetype)s; name="%(filename)s"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="%(filename)s"
%(mimed)s'''
# Функция возвращает mime-тип по расширению файла
# Используется словарь, сформированный во время инициализации класса
def getmimetypebyext(self, ext):
try:
mt = self.mimetypes[ext]
except:
mt = 'application/octet-stream'
return mt
# Функция формирует сообщение и отправляет его
def sendfiles(self, toaddr, filelist):
# заполняем словарь параметров, используемых в шаблоне
param = {}
param['boundary'] = '-=-=_BOUNDARY_adba_YRADNUOB_=-=-'
param['fromaddr'] = pymaconf.fromaddr
if pymaconf.backmail and pymaconf.backaddr:
param['toaddr'] = toaddr + '\nBcc: ' + pymaconf.backaddr
envtoaddr = [toaddr]
envtoaddr.append(pymaconf.backaddr)
else:
param['toaddr'] = toaddr
envtoaddr = [toaddr]
param['subject'] = pymaconf.defsubject
param['message'] = pymaconf.defmessage
param['codepage'] = pymaconf.mailcodepage
param['message'] = smtplib.base64.encodestring(param['message'])
msg= self.messagepart % param
# для каждого файла из списка формируем часть сообщения, объявляющего вложение
files = ''
print 'pysender: MIME-преобразование вложений:'
for filename in filelist:
files += filename + ','
param['filename'] = filename
base, ext = os.path.splitext(filename)
param['mimetype'] = self.getmimetypebyext(ext[1:])
print 'pysender: + %s...' % filename
ist = open(filename, 'rb').read()
param['mimed'] = smtplib.base64.encodestring(ist)
msg = msg + (self.attachpart % param)
# отправка сообщения
print 'pysender: соединение с %s...' % pymaconf.smtpserver
try:
session = smtplib.SMTP(pymaconf.smtpserver)
if pymaconf.authrequire:
session.login(pymaconf.authlogin,pymaconf.authpassword)
print 'pysender: выполняется отправка...'
session.sendmail(pymaconf.fromaddr, envtoaddr, msg)
print 'pysender: отправка завершена.'
session.quit()
pymalog.write('SENT: %s to %s' % (files, toaddr))
except:
print 'pysender: ОШИБКА ОТПРАВКИ.'
pymalog.write('ERROR: %s to %s' % (files, toaddr))
Центральной частью описанного здесь класса являются шаблоны сообщений messagepart и attachpart. В них широко применяется использование знакомест вида %(key)s, которые заполняются значениями, соответствующими ключам из словаря, указываемого после оператора «%». Например, строка param[‘message’] будет помещена в шаблон вместо конструкции %(message)s.
Шаблоны описывают вид сообщения в формате MIME. Для упрощения работы все, включая тело сообщения, мы кодируем в соответствии с Base64, для чего используется функция encodestring() класса smtp.base64. Каждое вложение также преобразовывается согласно Base64 и оформляется как mime-часть сообщения. Для определения mime-типа файла по его расширению используется файл соответствия mime.types, в качестве которого выступает слегка откорректированный одноименный файл, поставляемый вместе с Apache.
Главное, что нужно в нем сделать, – разбить строки, в которых одному mime-типу ставятся в соответствие несколько расширений, таким образом, чтобы в каждой строке фигурировало только одно расширение. Комментарии и строки без расширений удаляются. Файл, который использую я, можно будет найти на сайте журнала в разделе «Исходный код». Если определить mime-тип не удалось, используется значение «application/octet-stream».
После того как будет сформировано сообщение нужного формата, оно отправляется адресату с помощью функции sendmail() стандартного модуля smtplib. Помимо текста сообщения ей передаются адреса отправителя и получателя для формирования конверта. Если в конфигурационном файле включен режим отправки копий всех писем на указанный адрес (что может быть полезно как для контроля, так и в качестве плохонькой, но все же замены папки «Отправленные»), то к адресу получателя добавляется и указанный в pymaconf.py. Функция write() модуля pymalog заносит в лог-файл сообщение о результате отправки письма, чтобы в дальнейшем можно было отследить, что, когда и куда отправлялось. Сам модуль pymalog.py:
Листинг 6. Модуль pymalog.py
# -*- coding: cp1251 -*-
import time, os
name = os.path.dirname(__file__) + os.path.sep + 'pyma.log'
def write(str):
dt = time.strftime('%d.%m.%Y %H:%I:%S')
log = open(name, 'a+')
log.write('%s: %s\n' % (dt, str))
log.close()
if __name__ == '__main__':
write('qwerty')
Все. Можно вводить заветную командную строку и смотреть, что получилось. А получились две неприятности, с которыми нам предстоит побороться. Первая – наш код записан в cp1251, а FAR выводит строки в cp866. В итоге весь вывод, который происходит во время работы утилиты, имеет нечитаемый вид. Если переписать код в cp866, то текст сообщений будет выглядеть нормально, но нечитаемыми станут имена файлов, имеющие символы кириллицы (Windows использует для именования файлов cp1251). Наверняка указанную проблему можно было бы обойти проще, чем это сделал я, но использованное решение может быть применено в других случаях, а потому представляет определенную учебную ценность. Мы переопределим стандартные потоки ввода-вывода. Для этого напишем еще один модуль, mystd.py:
Листинг 7. Модуль mystd.py
# -*- coding: cp1251 -*-
import sys
from pymaconf import termcodepage, basecodepage
# Класс переопределения потока ввода
classmystdin:
def readline(self):
str = self.origin.readline()
return unicode(str, termcodepage).encode(basecodepage)
def setmystdin(self):
self.origin, sys.stdin = sys.stdin, self
def setorigin(self):
sys.stdin = self.origin
# Класс переопределения потока вывода
classmystdout:
def write(self, str):
self.setorigin()
print unicode(str, basecodepage).encode(termcodepage),
self.setmystdout()
def setmystdout(self):
self.origin, sys.stdout = sys.stdout, self
def setorigin(self):
sys.stdout = self.origin
Итак, что тут происходит? Класс mystdout имеет три функции – setmystdout(), назначает в качестве стандартного потока вывода sys.stdout сам этот класс, setorigin() возвращает прежнее значение, заблаговременно сохраненное в self.origin. А функция write() необходима, чтобы этот класс действительно мог играть роль потока вывода. В ней-то и происходит все самое интересное – мы возвращаем на место поток stdout, выводим в него переданную как параметр и перекодированную строку, и вновь назначаем потоком вывода класс mystdout. В итоге выводимые строки по-прежнему попадают на экран, но предварительно перехватываются и преобразуются в нужную кодировку.
Аналогично поступаем с sys.stdin. Классу mystdin нужен метод readline, который вызывает стандартный readline объекта sys.stdout (который сохранен в переменной self.origin) и возвращает считанную строку перекодированной.
Теперь становятся понятны загадочные строки в конце модуля send.py, в которых фигурируют объекты mi и mo. Они служат для переопределения потоков ввода-вывода.
Вторая неприятность заключается в том, что если имя файла содержит русские символы, то, будучи упакованным, оно исказится. Связано это с тем, что такие программы, как WinRAR, WinZip, 7-Zip и т. д., считают, что имена упакованных файлов должны быть в кодировке cp866. Но класс ZipFile стандартного модуля zipfile никаких преобразований имен файлов не выполняет, записывая их, как есть. Если предварительно преобразовать имя файла в нужную кодировку и в таком виде передать его функциям модуля, то возникает ошибка «Файл не найден», поскольку файла с полученным именем действительно не существует.
Ранее, когда я описывал отправку файлов по FTP, данная проблема также имела место, но была не столь актуальна, так как предполагалось, что и упаковка, и распаковка будут выполняться с помощью Python. В случае же электронной почты можно почти со стопроцентной уверенностью утверждать, что распаковываться архив будет чем угодно, но только не программой на Python.
Для частного решения указанной проблемы я слегка подправил библиотечный файл zipfile.py, размещающийся в каталоге Python, в подкаталоге Lib. Копируем этот модуль под именем zipfrusw.py, и в модуле myzip подгружаем этот измененный файл вместо zipfile. Именно это я и имел в виду, когда выше говорил о небольшой модификации myzip. Итак, в zipfrusw.py вносим следующие изменения. В исходную функцию close():
Листинг 8. Изменения в модуле zipfile: close
def close(self):
"""Close the file, and for mode "w" and "a" write
the ending records."""
.. .. .. .. .. ..
self.fp.write(centdir)
# self.fp.write(zinfo.filename)
try:
fn4zip = unicode(zinfo.filename,
pymaconf.termcodepage).encode('cp866')
self.fp.write(fn4zip)
except:
self.fp.write(zinfo.filename)
self.fp.write(zinfo.extra)
.. .. .. .. .. ..
Синим выделены добавленные строки, зеленым – исключенная. То есть здесь мы преобразуем имя файла в нужную кодировку перед тем, как оно будет записано в файл архива. Для перекодировки используем функцию Unicode(), которая формирует строку в кодировке UTF, с последующим вызовом функции encode(), которая преобразует Unicode-строку в указанную кодировку.
Чтобы модуль умел читать собственные творения, потребуются изменения в функцию getinfo():
Листинг 9. Изменения в модуле zipfile: getinfo
def getinfo(self, name):
"""Return the instance of ZipInfo given 'name'."""
try:
name = unicode(name, 'cp866').encode(pymaconf.termcodepage)
except:
pass
return self.NameToInfo[name]
и в _RealGetContents():
Листинг 10. Изменения в модуле zipfile: _RealGetContents
def _RealGetContents(self):
"""Read in the table of contents
for the ZIP file."""
.. .. .. .. .. ..
if self.debug > 2:
print centdir
filename = fp.read(centdir[_CD_FILENAME_LENGTH])
filename = unicode(filename, 'cp866').encode(pymaconf.termcodepage)
# Create ZipInfo instance to store file information
x = ZipInfo(filename)
.. .. .. .. .. ..
Теперь упакованные файлы сохраняют свои первоначальные имена при извлечении из архива, хотя должен признать, что всесторонние исследования я не проводил и не могу гарантировать, что подобный «хакинг» останется без последствий при другом использовании модуля. Я хотел лишь показать, что при желании исходные тексты стандартных модулей всегда можно подогнать под свои нужды.
Еще осталось сделать так, чтобы нашу утилиту можно было вызывать как «send», а не как «send.py». Для этого создадим bat-файл send.bat:
myprogspythonpython myprogsutilspymasend.py %1 %2 %3 %4 %5 %6 %7 %8 %9
К сожалению, в нем мы вынуждены использовать полные пути к файлам, что потребует редактирования каждый раз, когда нужно будет перенести утилиту в другое место. Кроме того, сценарию send.py в данном примере может быть передано только 9 параметров, что налагает ограничения на количество отправляемых за один раз файлов. Но зато командная строка стала выглядеть так, как нам хочется.
Итак, мы получили небольшую утилиту, которая позволяет отправлять файлы по электронной почте непосредственно из командной строки менеджера FAR. Благодаря использованию функции glob.glob() для формирования списка файлов мы получили возможность задавать в параметре files и шаблоны. Например, так мы отправим все файлы, имеющие расширение «py»:
sendtome files *.py as pyma
Утилита получилась достаточно гибкой – можно указать несколько файлов для отправки (или несколько шаблонов), задавать имя формируемого архива (по умолчанию используется имя первого файла или «archive.zip», если первым задан шаблон), отказаться от упаковки и передать файлы как есть. «Адресная книга» несколько упрощает ввод адресов электронной почты, позволяя указывать псевдонимы для тех адресов, которые вы наиболее часто используете. Чтобы не усложнять командную строку, тема и сопроводительный текст сообщения запрашиваются интерактивно. При желании такое поведение можно отключить, установив соответствующие переменные в pymaconf.py в ноль.
Из недостатков можно указать малоинформативный вывод сообщений об ошибках, не совсем точное следование стандартам, несколько громоздкий и неудобный для непосредственного просмотра формат лог-файла. Также в данной версии не предусмотрена одновременная отправка сразу на несколько адресов. В тело письма сейчас можно поместить только одну строку, поскольку нажатие клавиши Enter прекратит ввод. Однако, как и раньше, исходный код всегда доступен, и любой недостаток при наличии времени и желания можно легко превратить в достоинство.
Кстати, эта утилита замечательно работает и в FreeBSD. Нужно только подправить переменные в конфигурационном файле, отвечающие за кодировку, и вместо создания bat-файла просто переименовать send.py в send, снабдив его «магической» строчкой:
#!/usr/local/bin/python
Ну и не забыть сделать этот файл исполняемым и доступным для поиска по переменной окружения PATH. На моем сервере, где используется koi8-r, строки файла конфигурации, отвечающие за кодировки, выглядят таким образом:
Листинг 11. Изменения в pymaconf.py для FreeBSD
basecodepage = "cp1251"
termcodepage = "koi8-r"
mailcodepage = "cp1251"
fzipcodepage = "cp1251"
fsyscodepage = "koi8-r"
Кодировку сообщений в исходных текстах можно оставить как есть (параметр basecodepage), единственное, в этом случае имена упаковываемых файлов в диагностических сообщениях будут нечитаемыми. Исправить это можно введением нескольких дополнительных перекодировок, но их уже и без того достаточно.
На других платформах работоспособность утилиты не проверялась, однако причин для проблем вроде бы никаких нет, и если вашей системой поддерживается Python, то должен работать и рассмотренный код.