Возможно, ты уже умеешь писать простые сценарии на Python, но пробовал ли ты делать графические программы с настоящими работающими кнопочками? Поверь, это тоже крайне увлекательно и не очень сложно! Сегодня мы попрактикуемся в этом, написав на Python и Qt простой почтовик, который будет отправлять письма с аттачами.
Содержание
- Как написать почтовик на Python и Qt
- Создание интерфейса почтовика
- Создание основной части почтовика
- Настройки почты
- Заключение
Сразу оговоримся, что приложение будет с рядом ограничений: в частности, мы не будем даже приступать к чтению почты и не сможем выбирать почтовый сервис для отправки. К тому же письма из самопального почтовика могут чаще попадать в спам. Воспринимай это скорее как учебный пример, который поможет тебе освоить полезные навыки.
Создание интерфейса почтовика
Для начала давай создадим интерфейс будущего приложения. Для этого воспользуемся фреймворком для разработки кросс‑платформенного ПО — Qt 5. Потребуется скачать его и установить.
Откроем Qt Designer и в появившемся окне выбора поставим галочку на пункте Main Window.
Затем создадим окно при помощи готовых инструментов, а именно Label для текста, Line Edit для строк ввода, Text Edit для поля ввода текста, а также Push Button для кнопок. Разложить всё можешь так, как тебе понравится, главное — поставить все элементы, показанные на картинке, и запомнить их номера. Дальше будет понятно, зачем это.
Не забудь в поле ввода пароля поставить параметр
echoMode = Password, чтобы скрыть пароль, а также выключить поля, которые могут быть активны только после правильного ввода данных. Чтобы это сделать, необходимо в поле
enabled убрать галочку.
После завершения работы мое окно выглядит как на скриншоте.
Сохраняй документ, и получишь файл с расширением
ui, который и будет файлом интерфейса. Я назвал его
Main.ui. Теперь к коду!
Первым делом импортируем две библиотеки. Они нужны для работы с Qt 5.
from PyQt5.QtWidgets import * from PyQt5 import uic |
Теперь прописываем класс интерфейса и в нем все функции. У нас будет функция ввода (
login), функция прикрепления файла (
attach_sth) и функция отправки сообщений (
send_mail). Также в основе основ лежит функция вызова графического интерфейса с демонстрацией окна и активацией трех кнопок.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class MyGUI(QMainWindow): def __init__(self): super(MyGUI, self).__init__() uic.loadUi(‘Main.ui’, self) self.show() self.pushButton.clicked.connect(self.login) self.pushButton_2.clicked.connect(self.attach_sth) self.pushButton_3.clicked.connect(self.send_mail) def login(self): pass def attach_sth(self): pass def send_mail(self): pass app = QApplication([]) windows = MyGUI() app.exec_() |
Здесь интересна функция
app.exec_(). Она останавливает работу с терминалом, чтобы все взаимодействие с приложением шло только через графический интерфейс.
Код уже можно запустить! Ты увидишь окно с нарисованным тобой интерфейсом, но в нем пока что ничего не будет работать. Конечно, мы же еще не определили, как это должно происходить.
Создание основной части почтовика
Итак, задача — написать движок. Приступим.
Функция входа. В ней мы делаем две вещи: логинимся и создаем переменную
msg (переменная с содержанием письма).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
def login(self): try: self.server = smtplib.SMTP(self.lineEdit_3.text(), # Обращаемся к протоколу SMTP по нужному порту self.lineEdit_4.text()) # Отправляем запрос ELHO self.server.ehlo() # TLS-соединение self.server.starttls() # Снова отправляем запрос ELHO self.server.ehlo() self.server.login(self.lineEdit.text(), # Ввод логина и пароля self.lineEdit_2.text()) # Переводим все поля входа в выключенное состояние self.lineEdit.setEnabled(False) self.lineEdit_2.setEnabled(False) self.lineEdit_3.setEnabled(False) self.lineEdit_4.setEnabled(False) self.pushButton.setEnabled(False) # Переводим все поля письма во включенное состояние self.lineEdit_5.setEnabled(True) self.lineEdit_6.setEnabled(True) self.textEdit.setEnabled(True) self.pushButton_2.setEnabled(True) self.pushButton_3.setEnabled(True) # Создание переменной msg self.msg = MIMEMultipart() # При ошибке в логине и/или пароле except smtplib.SMTPAuthenticationError: # Создать окно massage_box = QMessageBox() # Окно заполнить текстом massage_box.setText(‘Недопустимые данные!’) # Переключиться на окно massage_box.exec() except: massage_box = QMessageBox() massage_box.setText(‘Не удалось выполнить вход!’) massage_box.exec() |
EHLO — это глагол расширенного протокола передачи простых сообщений (ESMTP), определенный в RFC 5321. Серверы ESMTP могут объявлять о своих возможностях во время начального подключения. Среди них — максимальный допустимый размер сообщения и поддерживаемые методы проверки подлинности. HELO — это более старая команда SMTP, определенная в RFC 821. Большинство SMTP-серверов поддерживают ESMTP и EHLO. Если сервер не поддерживает EHLO, вместо него можно использовать HELO.
Далее идет функция прикрепления вложений:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def attach_sth(self): # Открываем диалоговое окно выбора файлов options = QFileDialog.Options() # Смотрим все типы файлов filenames, _ = QFileDialog.getOpenFileNames(self, ‘Open File’, », ‘All Files (*.*)’, options=options) # Собираем файлы if filenames != []: for filename in filenames: attachment = open(filename, ‘rb’) filename = filename[filename.rfind(‘/’) + 1:] # Кодируем файл по стандарту MIME p = MIMEBase(‘application’, ‘octet-stream’) p.set_payload(attachment.read()) encoders.encode_base64(p) # Пишем имя файла p.add_header(‘Content-Disposition’, f‘attachment; filename={filename}’) self.msg.attach(p) # Ставим запятые после первого вложения if not self.label_8.text().endswith(‘: ‘): self.label_8.setText(self.label_8.text() + ‘,’) self.label_8.setText(self.label_8.text() + ‘ ‘ + filename) |
Создание той самой переменной
msg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
def send_mail(self): # Создаем окно с уточнением dialog = QMessageBox() dialog.setText(‘Вы хотите отправить сообщение?’) # Выбор ответов dialog.addButton(QPushButton(‘Да’), QMessageBox.YesRole) dialog.addButton(QPushButton(‘Нет’), QMessageBox.NoRole) if dialog.exec_() == 0: try: # От кого сообщение self.msg[‘From’] = ‘Xakep’ # Кому self.msg[‘To’] = self.lineEdit_6.text() # Тема self.msg[‘Subject’] = self.lineEdit_5.text() # Собираем сообщение self.msg.attach(MIMEText(self.textEdit.toPlainText(), ‘plain’)) text = self.msg.as_string() # Отправляем по пути self.server.sendmail(self.lineEdit.text(), self.lineEdit_6.text(), text) # Создаем окно с подтверждением massage_box = QMessageBox() massage_box.setText(‘Сообщение отправлено!’) massage_box.exec() except: # Создаем окно с ошибкой massage_box = QMessageBox() massage_box.setText(‘Не удалось отправить сообщение!’) massage_box.exec() |
Весь код можешь посмотреть на GitHub.
Настройки почты
Я выбрал почту Gmail, поскольку в нем можно выключить защиту от сторонних приложений. Многие другие почтовики ставят запрет на нее, а здесь достаточно перейти по ссылке и выставить разрешение.
Адрес SMTP для Gmail —
smtp.gmail.com, порт — 587. Для других почтовых клиентов можно посмотреть эти данные, просто загуглив их название со словом SMTP.
Заключение
Вот мы и написали свой базовый клиент для отправки писем с любыми вложениями. Как видишь, все, что нужно, — это нарисовать форму и написать обработчики, процесс очень простой и прямолинейный.
На всякий случай предупреждаю: можешь даже не пытаться использовать этот код без изменений в незаконных целях. Мы здесь никак не скрываем свой IP, никак не шифруем файлы и не обходим антивирусы.
РЕКОМЕНДУЕМ:
Система распознавания лиц на Python
Зато ты можешь использовать этот пример, чтобы добавить к своим приложениям удобный графический интерфейс.
(4 оценок, среднее: 4,75 из 5)
Загрузка…
Проектируем удобный почтовый клиент
Время на прочтение
6 мин
Количество просмотров 28K
Недавно, просматривая старые статьи Хабра, наткнулся на опрос о почтовых клиентах, который показал, что подавляющее большинство читателей предпочитает Gmail. Я сам пользуюсь им много лет, хотя пару раз в год безрезультатно пытаюсь “пересесть” на что-то поудобнее. С тех пор я стал думать о том, как можно было бы сделать Gmail лучше. Идей накопилось столько, что я решил создать собственный почтовый клиент. Любителей почитать про интерфейсы прошу под кат. И да поможет мне Фиттс.
Низкий старт. Портрет пользователя и позиционирование
В первую очередь стоит задуматься о позиционировании продукта. Обычно этим вопросом пренебрегают при проектировании интерфейсов и переходят сразу к дизайну. В итоге получается продукт для всех и ни для кого.
Я составил портрет своего целевого пользователя. Получился вот такой полностью вымышленный человек, под которого и будет создаваться продукт:
Интуиция мне подсказывает, что такой почтовый клиент лучше приживется сначала на Западе, а уже потом у нас. Поэтому я решил все материалы сразу делать на английском.
Начало
Конкурентов нужно знать в лицо. Я протестировал десяток веб-клиентов из опроса и сделал выводы, которые учел при дальнейшем проектировании. Когда я создавал прототип, прежде всего руководствовался не принципом “чтобы было не похоже на…”, а своими логическими соображениями, законами Фиттса и Хика, работами Раскина, Купера и других авторитетных проектировщиков.
Сразу запустил небольшой опрос по друзьям в соц.сети:
Результат весьма ожидаемый: люди хотят, чтобы было быстро, просто и красиво. Голосовала не совсем целевая аудитория, зато результат получен быстро и бесплатно: пока этого достаточно.
Самое главное: интерактивный прототип
Некоторые бросаются сразу делать дизайн. Честно говоря, я совершенно не понимаю, как можно отлаживать “механику” взаимодействия на графических макетах. В общем, начинаем с интерактивного html-прототипа в Axure, в котором можно было бы прямо по ходу проектирования тестировать и переделывать различные элементы взаимодействия.
Далее приведу наиболее интересные скриншоты с пояснениями. Если любопытно пощупать руками, в конце статьи вы найдете ссылку на прототип.
Итак, авторизуемся под аккаунтом Gmail, разрешаем приложению использовать данные, попадаем во входящие
Своему почтовому клиенту я дал временное рабочее название UXMail и буду использовать его только в рамках этой статьи.
На всякий случай уточню, что это лишь функциональный прототип, а значит все цвета, шрифты, иконки и формы — лишь условные, внимания на стиль обращать не стоит: все будет нормально “отрисовано” позже профессиональным дизайнером.
Навигация и письма
Для начала я решил максимально снизить шум в навигационной панели, поставив в меню лишь иконки — без подписей. Первые 10 минут пользователь может чувствовать себя слегка некомфортно из-за того, что сходу не скажет, где тут архив, где спам, а где отправленные. На этот случай предусмотрены стандартные всплывающие подсказки:
Я уверен, что этот временный недостаток с лихвой окупится при постоянном использовании.
И тут не обошлось без GTD и геймификации
Я уважаю принципы методологии Getting Things Done о “пустом инбоксе”, поэтому в интерфейсе старался всячески мотивировать не откладывать обработку писем, и как можно скорее очистить папку с входящими. Например, когда последнее письмо отправляется в архив, UXMail хвалит пользователя
Предполагается ротировать надписи и картинки, чтобы была мотивация узнать, как еще тебя похвалит почтовый клиент. Этакий элемент нынче модной геймификации.
При наведении на письмо во входящих появляется кнопка добавления этого письма в архив. Пользователю достаточно будет подвинуть курсор на несколько пикселей и кликнуть по иконке:
После чего письмо с анимацией “улетает” в иконку архива в меню.
Закон Фиттса как всегда безупречен
Для сравнения, то же самое действие — добавление в архив — в Gmail. Пользователь вынужден каждый раз проделывать пять(!) операций вместо двух:
- Подвести курсор к флагу
- Установить флаг
- Подвести курсор к кнопке “в архив”
- Кликнуть
- Перевести курсор на следующее письмо
Отвечаем на письмо
В интерфейсе входящего письма ничего концептуально нового нет. При клике поле ввода “разъезжается” до состояния, в котором удобно написать ответ
Менее популярные элементы интерфейса скрыты. Например, копия при ответе. Панель форматирования сильно приглушена цветом, но при наведении и клике инструменты “подсвечиваются”:
Отмена и подтверждение действий
К модальным подтверждениям типа “Вы уверены, что хотите …?” пользователи быстро привыкают и машинально нажимают “да” даже не вчитываясь. Поэтому лучше делать всплывающие уведомления: в таком случае у пользователя будет 3-4 секунды, чтобы передумать и отменить свое решение
Цепочки писем и микроцепочки
Некоторым это очень нравится, некоторым очень не нравится. Кажется, равнодушных уже почти не осталось. В комментариях к одной статье на Хабре цепочки писем даже сравнивали со свежей кинзой.
Лично я люблю свежую кинзу, но считаю, что цепочки писем — это изобретение непродуктивное и неудобное. Да, есть небольшой процент случаев, когда они оправданы, но даже в этих случаях сказываются последствия фундаментальных недочетов этого механизма:
- Не понятно, сколько новых писем появилось в цепочке. Просто появился один “непрочитанный” элемент — цепочка по теме
- Открывая цепочку, все письма помечаются как “прочитанные”. В результате можно пропустить новое письмо
- Непонятно, к какому письму цепочки будут применены общие действия (ответить, переслать и другие). Хотя в Gmail это уже частично решено, чего не скажешь о некоторых других клиентах
У цепочек есть и одно существенное преимущество: они позволяют проследить всю линию общения и вспомнить какую-то важную информацию. Признаться, я сначала тоже собирался делать цепочки, “приглушив” перечисленные недостатки рядом интерфейсных “костылей”, но при этом UXMail терял в простоте и минималистичности.
В итоге я решил ввести новое понятие — микроцепочки. Фактически, микроцепочка — это входящее сообщение и ответ пользователя на это сообщение. Такой механизм позволяет сохранить простоту и прямолинейность интерфейса без цепочек и получить определенные преимущества интерфейса с цепочками хотя бы в рамках одной итерации “входящее письмо — ответ на него”. В подавляющем большинстве случаев этого достаточно, чтобы восстановить в голове смысл переписки
Проверяйте спам чаще
Папку со спамом я сознательно “вытащил” на верхний уровень. Я считаю, что пользователям стоит хотя бы изредка проверять спам на предмет важных писем, потому что спам-фильтры пока не 100% корректно работают. Соответственно, предполагается такое взаимодействие: пользователь пробегает глазами список писем в спаме, с помощью быстрой кнопки отправляет некоторые письма обратно во входящие, а в конце списка его ждет большая кнопка “Удалить весь спам безвозвратно” — и далее будет опять хвалебная надпись и картинка
Метки и фильтры: их-то за что?
А вот фанатам меток и фильтров UXMail скорее всего не понравится: фильтры придется настраивать через интерфейс Gmail, а меток вообще нет — остались только папки
Я вижу следующие недостатки использования папок совместно с фильтрами:
- Нарушается принцип “одного пустого инбокса”. Т.е. чтобы продуктивно обрабатывать почту нужно следить за тем, чтобы сразу несколько папок были пустыми или хотя бы просто обработанными
- Увеличивается вероятность пропустить важное письмо: человек — не робот: может банально забыть о какой-то папке и не проверить ее
- Если посадить два человека рядом и слать им одни и те же письма, то, рискну предположить, обрабатывать поток писем быстрее будет тот, кто получает все в одну папку, т.к. он не тратит время на переключение между папками
По поводу меток я в сомнении: пока решил их вообще не поддерживать. Метки — это когда все письма остаются в общем “инбоксе”, просто помечаются словами “работа”, “учеба” и так далее. Я как-то пользовался метками, потом перестал, т.к. обратил внимание, что они улучшают продуктивность лишь ненадолго. Потом приходится систематически тратить время на “ревью” созданных меток и добавление новых. Это при том что в 9 из 10 случаев и так понятно, какое письмо по учебе, а какое — по работе.
Управление вложениями
Еще одна фича, которая, на мой взгляд, может пользоваться хорошим спросом — централизованное управление вложениями. Сейчас в почтовых клиентах достаточно трудно искать полученные файлы, особенно, если не помнишь ни имени отправителя, ни даты. В UXMail можно пролистать на одном экране все вложения и отфильтровать их по дате, отправителю или теме:
Все-все, уже закругляемся. Настройки
Я изучил интерфейсы настроек нескольких популярных почтовых клиентов, подумал и убрал все лишнее. Осталось только 4 раздела: почтовые ящики, подписи, автоответчик и уведомления
Каждая настройка в обязательном порядке сопровождается пояснительным текстом и иллюстрацией того как это будет выглядеть в интерфейсе. Некоторые настройки продублированы в “повседневных” интерфейсах. Например, можно добавить еще один адрес отправителя прямо из контекста создания нового письма:
А когда пользователь включает автоответчик, в верхней панели статично закрепляется сообщение о том, что включен автоответчик
Это должно хоть немного мотивировать пользователей включать автоответчик только по “уважительным причинам” вроде отпуска или болезни. Надеюсь, станет чуть меньше получателей, от которых в ответ на каждое письмо автоматически приходят страшные слова в духе “Спасибо, я получил вашего сообщение”.
Пожалуй, на этом все. В интерактивном прототипе вы найдете еще много занятных механизмов, с которыми можно “поиграться”, но описывать все в статье было бы совсем объемно и скучно (боюсь, с объемом я и так немного перестарался).
Какие планы?
Собрать отзывы с Хабра, доработать прототип, сделать графический дизайн и промо-материалы, привлечь инвестиции на разработку через краудфандинговые платформы.
И зачем я это все написал?
Я — человек жутко ленивый до деятельности, которая потенциально может быть бесполезна, поэтому эту статью я написал не только чтобы немного развлечь читателей и себя, но и привлечь авторитетное мнение хабрасообщества и воззвать к жестокой и исключительно обоснованной критике описанных выше идей и решений. Я собираюсь сделать действительно крутой продукт, и ваши мнения и доводы мне помогут лучше чем что-либо ещё. Благодарю за внимание.
Передача данных
Формат обмена данными
Класс POPClient
Внутренний класс POPCommand
Внутренний класс POPResponse
Класс POPException
Команды протокола РОРЗ
Листинг 1
Листинг 2
Написание программ на Java представляет собой весьма интересное
- Внутренний класс POPCommand
- Внутренний класс POPResponse
Команды протокола РОРЗ
Листинг 1
Листинг 2
Написание программ на Java представляет собой весьма интересное занятие, поскольку можно абстрагироваться от многих деталей, с которыми неизбежно сталкиваются пользователи Cи++, Паскаля и других языков программирования третьего поколения. С самого начала специалисты корпорации Sun задумали Java как язык, стирающий разницу между разрозненными компьютерами и сетями, и, видимо, поэтому значительная часть библиотеки классов Java абстрагирует сетевые операции и транспортные протоколы.
Простейшая Java-программа, принимающая почту с сервера, — это по силам каждому.
Передача данных
Прежде чем написать почтовый клиент, неплохо было бы изучить язык, на котором общаются почтовый клиент и сервер, — протокол POP3 (Post Office Protocol). Именно с помощью этого протокола в большинстве случаев принимаются почтовые сообщения такими популярными почтовыми программами, как Microsoft Outlook и Express Netscape Messanger. Для начала примите к сведению, что все почтовые операции на нижнем уровне производятся с применением транспортного протокола TCP. Сокет сервера передает данные в формате POP3 TCP-пакетами в порт 110 (более ранняя вторая версия протокола POP использовала порт 109).
Кроме того, необходимо знать, что команды от клиента к серверу пересылаются в виде текстовых строк, завершающихся парой символов «перевод каретки» (CR) и «перевод строки» (LF). Получив такое сообщение, почтовый сервер приступает к тяжелой работе: либо отвечает на ваши команды, либо пересылает сообщения, имеющиеся в вашем почтовом ящике. Вот здесь начинается настоящее мучение, ибо те, кто разрабатывал протокол POP3, променяли удобство использования на простоту реализации, не подумав о программистах, которым с ним работать.
Формат обмена данными
Команды, которые вы отправляете серверу, делятся на две категории: без параметров и содержащие параметры. Если простые команды без параметров состоят всего из одного слова, то в команды с параметрами добавляются текстовые параметры (или цифровые параметры, преобразованные в текстовую форму и разделенные пробелами). Каждый параметр может иметь длину до 40 символов. В качестве примера простой команды можно привести команду «STAT», которая запрашивает состояние вашего почтового ящика. Примером команды с параметром может служить команда «USER Mitrich», сообщающая серверу, что к нему подключается пользователь с именем Mitrich. Прежде чем отправиться дальше, загляните в таблицу и ознакомьтесь со всеми командами, которые описывает протокол POP3.
Возвращаемые сообщения могут быть двух видов: одно- и многострочные. В первом случае сервер возвращает одну строку; она начинается с признака статуса +OK или -ERR, говорящих об успехе или неудаче выполнения последней команды. Следом за признаком статуса сервер передает полезную информацию различного назначения. Завершается строка символами ‘
‘ и ‘
‘. Если же возвращаемое сообщение многострочное, то оно передается строка за строкой, каждая из которых завершается ‘
‘ и ‘
‘, а в качестве признака окончания передачи используется строка из единственного символа ‘.’ (точка), также оканчивающаяся символами возврата каретки и перевода строки.
Класс POPClient
Ядро почтового клиента — это класс POPClient, реализованный на языке Java (см. листинг 1). Он предоставляет пользователю несколько полезных функций для управления посылкой команд серверу и получения ответа от последнего. Класс POPClient вмещает в себя два внутренних класса — POPCommand и POPResponse. Такого разбиения единого, казалось бы, класса на подклассы требует сама суть объектно-ориентированного программирования: каждая сущность должна быть реализована как отдельный класс. А поскольку и POPCommand и POPResponse являются вспомогательными по отношению к главному классу POPClient, то они выполнены не как независимые классы, а как классы внутренние. Средства пакета JDK 1.1 предусматривают решение подобных задач.
Внутренний класс POPCommand
Класс POPCommand, как вы, наверное, уже догадались, является инкапсуляцией команды протокола POP3. Для его создания нет конструктора, поэтому этим занимается компилятор Java. С помощью объектов этого класса формируются команды для посылки серверу и производится собственно посылка команды. Кроме того, обязанностью этого класса является чтение ответа сервера на посланную ранее команду. Самую черную работу на нижнем уровне выполняют следующие методы:
Нетрудно догадаться, что это всего лишь оболочки для посылки команд с аналогичными именами. Внутри этих методов происходит стандартная обработка: имеющийся аргумент проверяется, передается вспомогательному методу transactCommand() и затем возвращается в виде true (команда прошла и обработана сервером) или false (произошла ошибка). Когда же сервер прислал вместо вразумительного ответа «тарабарщину», происходит возбуждение исключения под названием POPException (о нем ниже в разделе «Класс POPException»).
Метод transactCommand принимает текстовую строку, которая соответствует посылаемой команде, передает ее методу sendCommand(), затем читает ответ сервера с помощью метода readCommandResponse(). В заключение вызывается метод isSucceed() внутреннего класса POPResponse. Этот метод возвращает результат последней транзакции.
Если работа sendCommand() достаточно понятна, то работа метода readCommandResponse() требует некоторого пояснения. В начале своей работы этот метод создает объект класса StringBuffer для временного хранения данных. Потом из потока ввода, подключенного к сокету, командой ir.readLine() читается строка ответа, которая добавляется к буферу строки методом append(). В момент окончания работы метода значение буфера преобразуется в строку методом toString() и записывается во внутреннее хранилище класса POPResponse методом setBuff().
Но не все так просто, как может показаться с самого начала. Дело в том, что читать данные из потока сокета можно, а вот установить момент, когда они считаны до конца, нельзя. И документация в этом деле, увы, ничем не поможет. Там рекомендовано читать строки из сокета до тех пор, пока возвращаемое значение не будет равно null. Но уверяю вас, этого момента вы не дождетесь. Точно так же вы не сможете воспользоваться методом read() для чтения из-за того, что и он блокируется при достижении конца передачи данных. Со стороны это выглядит как зависание, но на самом деле поток, в котором вызывается метод чтения данных, просто переходит в состояние ожидания очередной порции информации. Поэтому мы идем на компромисс и в дополнение к методу readCommandResponse() реализуем метод readMessage(), который предназначается лишь для считывания многострочного почтового сообщения. Конец передачи данных мы определяем по строке, состоящей из одного символа точки «.». Метод readMessage() вызывается из командного метода retr() и, вместо того чтобы, как другие команды, сделать вызов метода transactCommand(), выполняет следующие три строки на Java:
sendCommand("RETR " + Integer.toString(number)); readMessage(); return response.isSucceed();
Обратите внимание, что многие методы класса POPCommand объявлены как private, чтобы предотвратить прямой доступ к ним.
Внутренний класс POPResponse
Следующий внутренний класс POPResponse отвечает за хранение полученных от сервера данных и предоставление удобного интерфейса для их обработки. Все данные, принятые от сервера, сохраняются методом setBuff() в переменной buff, которая есть не что иное, как ссылка на строку класса String. Для вспомогательных нужд есть метод getBuff, с помощью которого можно получить содержимое хранилища в том виде, в каком оно было получено от почтового сервера.
Для предварительной обработки данных вы можете вызывать еще два полезных метода: cutOffStatus() и getServerComment(). Первый из них берет содержимое временного хранилища, отрезает от него признак статуса (+OK или -ERR) и возвращает оставшуюся строку. Второй метод возвращает текстовое сообщение, которое было передано POP-сервером сразу за признаком статуса. Следует, однако, отметить, что применять этот метод можно лишь после того, как серверу была передана команда, следом за которой сервер непременно пришлет сообщение (см. таблицу), иначе возвращенная методом getServerComment() строка будет бессмысленна.
Настал черед главного класса POPClient, с которым нам нужно разобраться. Сразу после объявления его самого описывается странная на первый взгляд переменная debug. Значение этой переменной управляет включением и выключением вывода отладочных сообщений в поток System.out. Если установить значение true, то специально написанный метод logText() будет посылать любую текстовую строку, которую вы ему передадите в окно консоли. После окончания отладки класса, вы можете установить debug в состояние false, и ваши строки, передаваемые logText(), уже не будут выводиться. Мало того, компилятор автоматически уберет посылки текста в окно консоли, оптимизируя код. Данная возможность походит на директивы условной компиляции препроцессора языков Cи и Cи++. О другой функции метода logText() мы поговорим чуть позже.
В начале класса POPClient объявляется несколько полей, имеющих следующее назначение:
в файл протокола.
В конструкторе по умолчанию производится создание новых экземпляров объектов внутренних классов POPCommand и POPResponse для дальнейшей работы. Если вам требуется протоколировать работу вашего почтового клиента, то воспользуйтесь другим конструктором, принимающим имя файла, который будет служить файлом протокола. Сначала этот конструктор вызовет конструктор по умолчанию, инициализируя таким образом внутренние классы, а затем создаст поток вывода данных в файл. Если таковой поток не удается создать, то возникает исключение ввода-вывода, которое мы перехватываем, и устанавливаем флаг разрешения протокола в положение «отключено» (false).
Для забывчивых программистов в классе предусмотрен финализатор — метод finalize(), вызываемый сборщиком мусора в момент завершения работы класса. Финализатор вызывает метод disconnect(), отключающий клиента от почтового сервера, если программист по каким-либо причинам не вызывал метод disconnect().
Для соединения с POP-сервером создаваемый нами класс оснащен методом connect(), который достаточно прост. На первом этапе connect() создает сокет для присоединения к серверу с заданным именем и портом. Если сервер с именем hostName не существует, то после некоторого ожидания будет возбуждено исключение UnknownHostException. То же произойдет при возникновении проблем с вводом-выводом данных — будет сгенерировано исключение IOException. Мы перехватываем эти исключения и генерируем свое — POPException. В качестве параметра конструктора задается константа, определяющая вид исключения, которое мы хотим возбудить.
После создания сокета, мы вызываем его методы getInputStream()и getOutputStream(), чтобы получить ссылки на потоки ввода и вывода сокета, через которые мы будем посылать и получать данные. Для удобства работы полученные ссылки преобразуются в экземпляры объектов классов BufferedReader и PrintWriter, пришедших на смену морально устаревшим классам потоков BufferedInputStream и PrintStream. Остается считать ответ сервера:
command.readCommandResponse();
и определить, произошло ли соединение:
return response.isSucceed();
Для отключения от сервера существует метод disconnect(). Он работает довольно прямолинейно: посылает серверу команду QUIT и поочередно закрывает потоки протокола сокетов, а затем и сам сокет.
Метод logText(), который косвенно упоминался выше, служит для выполнения двух функций: запись строки, полученной через параметр text, в файл протокола, если протоколирование разрешено. Эти же данные пересылаются в окно консоли в том случае, если флаг debug задан как true.
Для получения доступа к почте необходимо вызвать метод login(), посылающий имя пользователя и пароль почтовому серверу. Если какой-либо из параметров неверен, то происходит генерация исключения POPException.
Еще два метода deleteMessage() и undoDeletes() служат для удаления и отмены удаления сообщений. Если вы хотите удалить сообщение, то вызываете deleteMessage(), передавая номер почтового сообщения, которое подлежит удалению. Учтите, что нумерация начинается с 1, как это принято у почтового сервера. Само сообщение при этом лишь помечается на удаление и в дальнейших транзакциях не участвует. Реально оно будет удалено в момент отключения клиента от сервера. Если же вы поняли, что совершили ошибку, то можете просто вызвать undoDeletes(), после чего пометка на удаление будет снята, и вы сможете читать это сообщение. Правда, придется заново загрузить все почтовые сообщения на локальный компьютер.
Класс POPException
Следуя концепции объектно-ориентированного программирования, просто необходимо создать свой собственный класс обработки исключительной ситуации — POPException. Он не только расширяет стандартный класс исключения Exception, но и добавляет специальное расширение — поле why, хранящее в виде числа причину возникновения исключения. Для удобства в классе определяются несколько констант, дающих этому числу осмысленное название. Если вы, создавая объект класса POPException, зададите в его конструкторе причину сбоя, то внутри класса вспомогательный метод assignMessage() подберет соответствующее текстовое сообщение. Перехватив исключительную ситуацию класса POPException, можно получить или разумное текстовое описание методом getMessage(), или же код причины методом why().
Завершая описание, советуем в дополнение к протоколу POP3 ознакомиться со следующими рабочими документами:
RFC821 — Simple Mail Transfer Protocol;
RFC1321 — The MD5 Message-Digest Algorithm.
Команды протокола РОРЗ
Успех и неудача выполнения команды отмечаются признаками статуса +ОК или -ERR соответственно
Команда |
Назначение |
Возможные возвращаемые значения |
USER <имя пользователя> | Посылка имени пользователя серверу | +ОК <комментарий сервера> — если имя пользователя правильное -ERR <комментарий сервера> — если имя пользователя неверное |
PASS <пароль> | Посылка пароля серверу | +ОК<комментарий сервера> — если пароль принят сервером -ERR <комментарий сервера> — если пароль неверный или с почтовым ящиком уже кто-то работает |
QUIT | Окончание сеанса работы | +ОК |
STAT | Получить состояние почтового ящика | +ОК <кол-во сообщений> <общий размер всех сообщений> |
UST [<номер сообщения>] | Получить параметры всех сообщений в ящике пользователя. Если задан номер сообщения, то будут получены только его параметры |
+ОК <параметры сообщений> -ERR <комментарий сервера> — если запрошенного сообщения в ящике нет. Возвращаемые параметры сообщений зависят от того, был ли задан номер сообщения. Если — да, то сразу после +ОК следует сообщение сервера. Затем строка за строкой передаются параметры всех сообщений в формате <номер сообщения> <размер сообщения> |
RETR <номер сообщения> | Получить сообщение с сервера | +ОК <тест запрошенного сообщения> — если команда прошла удачно -ERR <комментарий сервера> — если запрошенное сообщение отсутствует на сервере |
DELE <номер сообщения> | Пометить сообщение на сервере как удаленное. Реально оно будет удалено после команды QUIT |
+ОК <комментарий сервера> — если сообщение было помечено на удаление -ERR <комментарий сервера> — если сообщение не существует или уже отмечено как удаленное |
NOOP | Пустая операция | +ОК |
RSET | Отменить удаление удаление сообщений, помеченных как удаленные |
+ОК <комментарий сервера> |
ТОР <номер сообщения> <кол-во строк> | Считать заголовок сообщения и первые строки в количестве, заданном параметром <кол-во строк> |
+ОК Далее строка за строкой передается заголовок сообщения. За ним следует пустая строка и, если имеется второй параметр, передаются начальные строки сообщения |
UIDL [<номер сообщения>] | Получить уникальные идентификаторы всех сообщений в ящике пользователя. Если задан номер сообщения, то будет получен только его идентификатор |
+ОК <параметры сообщений> -ERR <комментарий сервера> — если запрошенного сообщения в ящике нет. Возвращаемые параметры сообщений зависят от того, был ли задан номер сообщения. Если — да, то сразу после +ОК идут номер запрошенного сообщения и его идентификатор. Если команда вызвана без параметра, то после статуса +ОК следует сообщение сервера. Затем строка за строкой передаются параметры всех сообщений в формате <номер сообщения> <идентификатор> |
APOP <имя пользователя> <дайджест> | Осуществляет подключение к почтовому серверу по закодированной алгоритмом MD5 строке, защищая транзакцию от разглашения пароля пользователя |
+ОК <комментарий сервера> — если имя пользователя или дайджест соответствуют имеющемуся почтовому ящику пользователя -ERR <комментарий сервера> — если имя пользователя или дайджест неверны |
Листинг 1
// Базовый класс для приема сообщений от // почтового сервера (с использованием // протокола POP3). // Формируем пакет package Mitrich.mail; // Импортируем нужные классы import java.io.*; import java.net.*; import java.util.Enumeration; import java.util.Vector; import java.util.StringTokenizer; // Вспомогательный класс. См. листинг 2 import Mitrich.mail.POPException; // Главный класс реализации протокола POP3 public class POPClient { // После окончания отладки установить false private final static boolean debug = true; // Номер порта POP3 public final static short POP3_PORT = 110; // Константы класса private final String EOL = " "; private final String S_OK = "+OK"; private final String S_ERR = "-ERR"; // Поля класса private POPCommand command = null; private POPResponse response = null; private Socket socket = null; private BufferedReader ir = null; private PrintWriter ow = null; private PrintWriter log = null; private Vector messages = null; private boolean logEnabled = false; // Конструктор по умолчанию public POPClient() { // Создаем экземпляр класса команды command = this.new POPCommand(); // Создаем экземпляр класса ответа сервера response = this.new POPResponse(); } // Этот конструктор создает файл протокола // с именем, заданным параметром logName public POPClient(String logName) { this(); logEnabled = true; try { // Создадим поток вывода в файл протокола // с автоматическим сбросом буферов на диск log = new PrintWriter( new FileOutputStream(logName, true), true); } catch(IOException e) { // Возникла проблема с созданием файла протокола // Протоколирование отключается logEnabled = false; } } // Финализатор protected void finalize() throws Throwable { // Отсоединиться от почтового сервера, если // пользователь забыл вызвать метод disconnect() disconnect(); } // Установить соединение с почтовым сервером public boolean connect(String hostName, int portNumber) throws POPException { try { logText("Creating a socket..."); // Создаем сокет socket = new Socket(hostName, portNumber); logText("Creating an input stream..."); // Получаем ссылку на поток ввода данных от сокета ir = new BufferedReader( new InputStreamReader( socket.getInputStream())); logText("Creating an output stream..."); // Получаем ссылку на поток вывода данных в сокет ow = new PrintWriter( new DataOutputStream( socket.getOutputStream()), true); // Слушаем ответ сервера command.readCommandResponse(); // Ответ +OK или -ERR ? return response.isSucceed(); } catch(UnknownHostException e) { logText("Host unknown"); // Заданный адрес сервера недействителен throw new POPException(POPException.HOST_UNKNOWN); } catch(IOException e) { logText("Creating an I/O channel failed"); // Ошибка сокета. Возможно, сервер отключился throw new POPException(POPException.SOCKET_ERROR); } } // Отключаемся от сервера public void disconnect() { try { logText("Disconnecting... "); // Посылается запрос на отключение command.quit(); if(ir != null) { ir.close(); ir = null; } if(ow != null) { ow.close(); ow = null; } if(socket != null) { socket.close(); socket = null; } if(log != null) { log.close(); log = null; } } catch(Exception e) { logText("Disconnection failed");} } // Записать строку в файл протокола public void logText(String text) { if(logEnabled) log.println(text); if(debug) System.out.println(text); } // Входим в почтовый сервер public void login(String name, String password) throws POPException { logText("Sending the user name..."); // Передаем имя пользователя серверу if( !command.user(name) ) throw new POPException(POPException.BAD_NAME); logText("Sending the password..."); // Если пароль есть, то передаем его серверу if(password != null) if( !command.pass(password) ) throw new POPException(POPException.BAD_PASSWORD); } // Загрузить всю почту на компьютер пользователя public void downloadMessages() throws POPException { StringTokenizer st = null; String tmpStr = null; // Создаем пустой вектор для хранения сообщений messages = new Vector(); // Перебираем сообщения на сервере for(int i = 1; command.list(i); i++) { // Если сообщение с заданным номером имеется, // считать его с сервера if(command.retr(i)) { // Найти место в данных, где находится первая // отметка конца строки int offset = response.buff.indexOf(EOL); // Записать сообщение в вектор, отрезав от // него строку статуса messages.addElement( response.buff.substring( offset + EOL.length())); } else throw new POPException(POPException.RETR_ERROR); } } // Возвращает пользователю текст сообщения. // Нумерация начинается с 1 public String getMessage(int number) { return (String)messages.elementAt(number - 1); } // Удаляет сообщение. Нумерация начинается с 1 public boolean deleteMessage(int number) throws POPException { if(command.dele(number)) { // Удалить сообщение из вектора messages.removeElementAt(number-1); return true; } return false; } // Отменить удаление сообщений, которые были // удалены вызовом метода deleteMessage() public boolean undoDeletes() throws POPException { if(command.rset()) { messages = null; // Перезагрузить почту с сервера downloadMessages(); return true; } return false; } // Этот внутренний класс предварительно сохраняет // необработанные данные class POPResponse { // Временное хранилище полученных данных private String buff = ""; // Возвращает true, если последняя команда // выполнена успешно, false - если // неудачно. Возбуждает исключение, если // сервер прислал неопределенный ответ public boolean isSucceed() throws POPException { boolean result = true; if(!buff.startsWith(S_OK)) { if(!buff.startsWith(S_ERR)) { throw new POPException(POPException.BAD_RESPONSE); } result = false; } return result; } // Записать данные во временный буфер protected void setBuff(String s) { buff = s; } // Считать данные из временного буфера protected String getBuff() { return buff; } // Возвратить данные без статусного признака protected String cutOffStatus() { int offset = buff.indexOf(' '); if(offset != -1) { String tmpStr = buff.substring(offset); return tmpStr.trim(); } return null; } // Получить комментарий сервера к последней // выполненной команде public String getServerComment() { String tmpStr = null; tmpStr = cutOffStatus(); int offset = tmpStr.indexOf(EOL); if(offset != -1) return tmpStr.substring(0, offset); return null; } } // Внутренний класс для представления команды, // посылаемой серверу class POPCommand { // Посылает строку почтовому серверу private void sendCommand(String command) throws POPException { logText("Sending command... "); try{ ow.println(command); } catch(IOException e) { throw new POPException(POPException.IO_ERROR); } } // Получить ответ на служебную команду private void readCommandResponse() throws POPException { logText("Reading response..."); StringBuffer tmpBuff = new StringBuffer(); try { tmpBuff.append(ir.readLine()); } catch(IOException e) { throw new POPException(POPException.IO_ERROR); } response.setBuff(tmpBuff.toString()); } // Считать почтовое сообщение private void readMessage() throws POPException { logText("Reading message..."); String tmpStr = new String(""); StringBuffer tmpBuff = new StringBuffer(); try { // Читать строку за строкой, пока не будет // найдена строка-терминатор. while(!(tmpStr = ir.readLine()).equals(".")) tmpBuff.append(tmpStr + " "); } catch(IOException e) { throw new POPException(POPException.IO_ERROR); } tmpStr = tmpBuff.toString(); response.setBuff(tmpStr); } // Метод-оболочка для выполнения типичной команды private boolean transactCommand(String command) throws POPException { sendCommand(command); readCommandResponse(); return response.isSucceed(); } // Команды передачи имени пользователя public boolean user(String name) throws POPException { return transactCommand("USER " + name); } // Команды передачи пароля public boolean pass(String password) throws POPException { return transactCommand("PASS " + password); } // Команда завершения сеанса работы с почтой public boolean quit() throws POPException { return transactCommand("QUIT"); } // Команда удаления сообщения public boolean dele(int number) throws POPException { if(number != 0) return transactCommand("DELE " + Integer.toString(number)); else return false; } // Команда отмены удаления public boolean rset() throws POPException { return transactCommand("RSET"); } // Получить информацию о сообщении public boolean list(int number) throws POPException { if(number != 0) return transactCommand("LIST " + Integer.toString(number)); else return false; } // Команда чтения сообщения с сервера public boolean retr(int number) throws POPException { if(number != 0) { sendCommand("RETR " + Integer.toString(number)); readMessage(); return response.isSucceed(); } else return false; } } }
Листинг 2
// Вспомогательный класс представляет исключения, // которые могут возникнуть в процессе работы // Автор - Дмитрий Рамодин // Изд. дом "Открытые Системы" // // Формируем пакет package Mitrich.mail; public class POPException extends Exception { // Константы типов исключительных ситуаций public static final int NOT_AVAILABLE = 0; public static final int BAD_RESPONSE = 1; public static final int BAD_NAME = 2; public static final int BAD_PASSWORD = 3; public static final int SOCKET_ERROR = 4; public static final int HOST_UNKNOWN = 5; public static final int IO_ERROR = 6; public static final int RETR_ERROR = 7; // Причина возникновения исключения private static int why = NOT_AVAILABLE; // Конструктор по умолчанию public POPException() { super(); } // Конструктор со строкой public POPException(String message) { super(message); } // Конструктор, в котором задается причина // исключительной ситуации public POPException(int reason) { super(POPException.assignMessage(reason)); } // Задать строку, соответствующую причине private static String assignMessage(int reason) { why = reason; switch(reason) { case BAD_RESPONSE: return new String( "Bad response from the mail server "); case SOCKET_ERROR: return new String( "Socket I/O couldn't be established "); case BAD_NAME: return new String( "There is not such user name "); case BAD_PASSWORD: return new String( "Invalid password "); case HOST_UNKNOWN: return new String( "Wrong hostname "); case IO_ERROR: return new String( "I/O operation error. "); case RETR_ERROR: return new String( "Fatal error occured during message reading "); default: return new String( "Unknown POPClient failure "); } } // Возвращает причину исключительной ситуации public int why() { return why; } }
IMailPlus.zip
Introduction
The code here is written under the common creativity license you could download the project from the link above or clicking on the next link IMailPlus.zip It has been enspired from Rodolfo Finochietti POP3 Project I have redesigned it and implement it and added to it SSL Support and optimized the design so that you could easily add other email clients below you will find a screen shot from the created demo application
This post will go with you step by step towards building your own Mail Client using C#, First to start I’ll start you need to first know the protocol you will be retrieving emails with I will discuss here the POP3 Protocol. The POP3 Protocols an application-layer Internet standard protocol along with IMAP4 they are the two most prevalent Internet standard protocols for e-mail retrieval. To understand how the protocol works lets examine a (Server — Client) communication example. First the server is waiting for connection from any client
S: <wait for connection on TCP port 110>
The Client opens a TCP Connection with the server
C: <open connection>
The Server then response with the +OK Ready State
S: +OK POP3 server ready <1123.69123552@mail.ramymostafa.com>
Then we authenticate using the username and password
C: USER username_here
S: +OK send PASS
C: PASS password
S: +OK Welcome.
C: LIST
S: +OK 2 messages (320 octets)
S: 1 120
S: 2 200
S: .
C: RETR 1
S: +OK 120 octets
S: <the POP3 server sends message 1>
S: .
C: DELE 1
S: +OK message 1 deleted
C: RETR 2
S: +OK 200 octets
S: <the POP3 server sends message 2>
The QUIT command will remove retrieved messages from the server and sign off.
C: QUIT
S: +OK dewey POP3 server signing off (maildrop empty)
C: <close connection>
S: <wait for next connection>
Designing the Mail Client
Now that we know how the POP3 protocol work we could start building our Mail Client. But before that let’s discuss the structure of an e-mail message.
A mail message consists of
|
So, Basically the email client should first connect to the server and then authenticate itself with the server then start to exchange the protocol messages with the server and parse the replies to extract from it the message data or the reply status itself etc…, It should be able also to retrieve the emails from the server and parse the message retrieve to extract from it the MailMessage Information.
Implementing the Email Client
Connect the Client To The Server using the given username, password and server it takes as parameters the Mail Server Domain Reference, The Mail Account User Name and Password.
public override void Connect(string server, string UserName, string Password) { try { if (_isConnected) { return; } if (!UseSSL) { Connect(server, PortNumber); string response = Response(); if (!response.Trim().StartsWith("+OK")) { } else { ExecuteCommand("USER", UserName); ExecuteCommand("PASS", Password); } _isConnected = true; } else { byte[] bts; int res; string responseString = ""; ResponseList.Clear(); Connect(server, PortNumber); inStream = new SslStream(this.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), new LocalCertificateSelectionCallback(SelectLocalCertificate)); inStream.AuthenticateAsClient(server); bts = new byte[1024]; res = inStream.Read(bts, 0, bts.Length); ResponseList.Add(Encoding.ASCII.GetString(bts, 0, res)); responseString = ExecuteCommand("USER", UserName); ResponseList.Add(responseString); responseString = ExecuteCommand("PASS", Password); ResponseList.Add(responseString); if (!responseString.Trim().StartsWith("+OK")) { } else _isConnected = true; } } catch (Exception ex) { } }
The GetMailList Method first Execute the Command List then it Retrieves a MetaMessageInfo from the Response String here the response string is parsed to retrieve Meta Information of the emails on the server like Number of Messages, and Messages Length
List result = new List(); string responseString = ExecuteCommand("LIST"); MetaMessageInfo info = new MetaMessageInfo(MailClientType.POP3,responseString);
Here we make a loop to retrieve messages until we retrieve all messages using the number of messages in the POPMessage constructor it extracts all the message info from the string to fill its attributes
for (int i = 1; i <= info.NumberOfMessages; i++) { responseString = ExecuteCommand("RETR", i.ToString(), true); POPMessage message = new POPMessage(responseString); message.Number = i.ToString(); message.Retrieved = true; if (message.MessageBoundaryId != null) result.Add(message); }
The Method that prase the response string to retrieve info from the Email is the LoadMethod
public override void Load(string messageString) { string message = messageString.Replace("+OK message follows", ""); Message = message; string messageIdPure = MessageBoundaryId; int bodyIndex = message.IndexOf("--" + messageIdPure, StringComparison.CurrentCultureIgnoreCase); int messageLength = message.Length - bodyIndex; if (bodyIndex < 0) return; string bodyString = message.Substring(bodyIndex, messageLength); string[] splitMessageOptions = new string[1]; splitMessageOptions[0] = "--" + messageIdPure; string[] messages = bodyString.Split(splitMessageOptions, StringSplitOptions.RemoveEmptyEntries); GetMessageBody(messages); }
to extract a header property I use
_from = GetHeaderValue(EmailHeaders.From);
Where the EmailHeaders.From is an enumerator with the headers and the GetHeaderValue Method extracts the required header value from the complete message string.
IMailPlusLibrary Usage
to use the library it’s simple all you need to to do is to do the below code
POPEmailClient popClient = new POPEmailClient();
popClient.UseSSL = account.UseSSL;
popClient.PortNumber = Convert.ToInt32(account.PortNumber);
popClient.Connect(account.Server, account.UserName, account.Password);
account.CurrentAccountMails = popClient.GetMailList();
Share Your Thoughts
Finally I hope that this project would be of a great help for you. Please feel free to contact me regarding anything you don’t understand in the code yo will find the library code fully XML documented. Also if you have any cool ideas or updates for it please share your thoughts here.
IMailPlus.zip
Introduction
The code here is written under the common creativity license you could download the project from the link above or clicking on the next link IMailPlus.zip It has been enspired from Rodolfo Finochietti POP3 Project I have redesigned it and implement it and added to it SSL Support and optimized the design so that you could easily add other email clients below you will find a screen shot from the created demo application
This post will go with you step by step towards building your own Mail Client using C#, First to start I’ll start you need to first know the protocol you will be retrieving emails with I will discuss here the POP3 Protocol. The POP3 Protocols an application-layer Internet standard protocol along with IMAP4 they are the two most prevalent Internet standard protocols for e-mail retrieval. To understand how the protocol works lets examine a (Server — Client) communication example. First the server is waiting for connection from any client
S: <wait for connection on TCP port 110>
The Client opens a TCP Connection with the server
C: <open connection>
The Server then response with the +OK Ready State
S: +OK POP3 server ready <1123.69123552@mail.ramymostafa.com>
Then we authenticate using the username and password
C: USER username_here
S: +OK send PASS
C: PASS password
S: +OK Welcome.
C: LIST
S: +OK 2 messages (320 octets)
S: 1 120
S: 2 200
S: .
C: RETR 1
S: +OK 120 octets
S: <the POP3 server sends message 1>
S: .
C: DELE 1
S: +OK message 1 deleted
C: RETR 2
S: +OK 200 octets
S: <the POP3 server sends message 2>
The QUIT command will remove retrieved messages from the server and sign off.
C: QUIT
S: +OK dewey POP3 server signing off (maildrop empty)
C: <close connection>
S: <wait for next connection>
Designing the Mail Client
Now that we know how the POP3 protocol work we could start building our Mail Client. But before that let’s discuss the structure of an e-mail message.
A mail message consists of
|
So, Basically the email client should first connect to the server and then authenticate itself with the server then start to exchange the protocol messages with the server and parse the replies to extract from it the message data or the reply status itself etc…, It should be able also to retrieve the emails from the server and parse the message retrieve to extract from it the MailMessage Information.
Implementing the Email Client
Connect the Client To The Server using the given username, password and server it takes as parameters the Mail Server Domain Reference, The Mail Account User Name and Password.
public override void Connect(string server, string UserName, string Password) { try { if (_isConnected) { return; } if (!UseSSL) { Connect(server, PortNumber); string response = Response(); if (!response.Trim().StartsWith("+OK")) { } else { ExecuteCommand("USER", UserName); ExecuteCommand("PASS", Password); } _isConnected = true; } else { byte[] bts; int res; string responseString = ""; ResponseList.Clear(); Connect(server, PortNumber); inStream = new SslStream(this.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), new LocalCertificateSelectionCallback(SelectLocalCertificate)); inStream.AuthenticateAsClient(server); bts = new byte[1024]; res = inStream.Read(bts, 0, bts.Length); ResponseList.Add(Encoding.ASCII.GetString(bts, 0, res)); responseString = ExecuteCommand("USER", UserName); ResponseList.Add(responseString); responseString = ExecuteCommand("PASS", Password); ResponseList.Add(responseString); if (!responseString.Trim().StartsWith("+OK")) { } else _isConnected = true; } } catch (Exception ex) { } }
The GetMailList Method first Execute the Command List then it Retrieves a MetaMessageInfo from the Response String here the response string is parsed to retrieve Meta Information of the emails on the server like Number of Messages, and Messages Length
List result = new List(); string responseString = ExecuteCommand("LIST"); MetaMessageInfo info = new MetaMessageInfo(MailClientType.POP3,responseString);
Here we make a loop to retrieve messages until we retrieve all messages using the number of messages in the POPMessage constructor it extracts all the message info from the string to fill its attributes
for (int i = 1; i <= info.NumberOfMessages; i++) { responseString = ExecuteCommand("RETR", i.ToString(), true); POPMessage message = new POPMessage(responseString); message.Number = i.ToString(); message.Retrieved = true; if (message.MessageBoundaryId != null) result.Add(message); }
The Method that prase the response string to retrieve info from the Email is the LoadMethod
public override void Load(string messageString) { string message = messageString.Replace("+OK message follows", ""); Message = message; string messageIdPure = MessageBoundaryId; int bodyIndex = message.IndexOf("--" + messageIdPure, StringComparison.CurrentCultureIgnoreCase); int messageLength = message.Length - bodyIndex; if (bodyIndex < 0) return; string bodyString = message.Substring(bodyIndex, messageLength); string[] splitMessageOptions = new string[1]; splitMessageOptions[0] = "--" + messageIdPure; string[] messages = bodyString.Split(splitMessageOptions, StringSplitOptions.RemoveEmptyEntries); GetMessageBody(messages); }
to extract a header property I use
_from = GetHeaderValue(EmailHeaders.From);
Where the EmailHeaders.From is an enumerator with the headers and the GetHeaderValue Method extracts the required header value from the complete message string.
IMailPlusLibrary Usage
to use the library it’s simple all you need to to do is to do the below code
POPEmailClient popClient = new POPEmailClient();
popClient.UseSSL = account.UseSSL;
popClient.PortNumber = Convert.ToInt32(account.PortNumber);
popClient.Connect(account.Server, account.UserName, account.Password);
account.CurrentAccountMails = popClient.GetMailList();
Share Your Thoughts
Finally I hope that this project would be of a great help for you. Please feel free to contact me regarding anything you don’t understand in the code yo will find the library code fully XML documented. Also if you have any cool ideas or updates for it please share your thoughts here.
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.swing.*;
import javax.swing.event.*;
// The E-mail Client.
public class EmailClient extends JFrame {
// Message table’s data model.
private MessagesTableModel tableModel;
// Table listing messages.
private JTable table;
// This the text area for displaying messages.
private JTextArea messageTextArea;
/* This is the split panel that holds the messages
table and the message view panel. */
private JSplitPane splitPane;
// These are the buttons for managing the selected message.
private JButton replyButton, forwardButton, deleteButton;
// Currently selected message in table.
private Message selectedMessage;
// Flag for whether or not a message is being deleted.
private boolean deleting;
// This is the JavaMail session.
private Session session;
// Constructor for E-mail Client.
public EmailClient() {
// Set application title.
setTitle(«E-mail Client»);
// Set window size.
setSize(640, 480);
// Handle window closing events.
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
actionExit();
}
});
// Setup file menu.
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu(«File»);
fileMenu.setMnemonic(KeyEvent.VK_F);
JMenuItem fileExitMenuItem = new JMenuItem(«Exit»,
KeyEvent.VK_X);
fileExitMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionExit();
}
});
fileMenu.add(fileExitMenuItem);
menuBar.add(fileMenu);
setJMenuBar(menuBar);
// Setup buttons panel.
JPanel buttonPanel = new JPanel();
JButton newButton = new JButton(«New Message»);
newButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionNew();
}
});
buttonPanel.add(newButton);
// Setup messages table.
tableModel = new MessagesTableModel();
table = new JTable(tableModel);
table.getSelectionModel().addListSelectionListener(new
ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
tableSelectionChanged();
}
});
// Allow only one row at a time to be selected.
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Setup E-mails panel.
JPanel emailsPanel = new JPanel();
emailsPanel.setBorder(
BorderFactory.createTitledBorder(«E-mails»));
messageTextArea = new JTextArea();
messageTextArea.setEditable(false);
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
new JScrollPane(table), new JScrollPane(messageTextArea));
emailsPanel.setLayout(new BorderLayout());
emailsPanel.add(splitPane, BorderLayout.CENTER);
// Setup buttons panel 2.
JPanel buttonPanel2 = new JPanel();
replyButton = new JButton(«Reply»);
replyButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionReply();
}
});
replyButton.setEnabled(false);
buttonPanel2.add(replyButton);
forwardButton = new JButton(«Forward»);
forwardButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionForward();
}
});
forwardButton.setEnabled(false);
buttonPanel2.add(forwardButton);
deleteButton = new JButton(«Delete»);
deleteButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionDelete();
}
});
deleteButton.setEnabled(false);
buttonPanel2.add(deleteButton);
// Add panels to display.
getContentPane().setLayout(new BorderLayout());
getContentPane().add(buttonPanel, BorderLayout.NORTH);
getContentPane().add(emailsPanel, BorderLayout.CENTER);
getContentPane().add(buttonPanel2, BorderLayout.SOUTH);
}
// Exit this program.
private void actionExit() {
System.exit(0);
}
// Create a new message.
private void actionNew() {
sendMessage(MessageDialog.NEW, null);
}
// Called when table row selection changes.
private void tableSelectionChanged() {
/* If not in the middle of deleting a message, set
the selected message and display it. */
if (!deleting) {
selectedMessage =
tableModel.getMessage(table.getSelectedRow());
showSelectedMessage();
updateButtons();
}
}
// Reply to a message.
private void actionReply() {
sendMessage(MessageDialog.REPLY, selectedMessage);
}
// Forward a message.
private void actionForward() {
sendMessage(MessageDialog.FORWARD, selectedMessage);
}
// Delete the selected message.
private void actionDelete() {
deleting = true;
try {
// Delete message from server.
selectedMessage.setFlag(Flags.Flag.DELETED, true);
Folder folder = selectedMessage.getFolder();
folder.close(true);
folder.open(Folder.READ_WRITE);
} catch (Exception e) {
showError(«Unable to delete message.», false);
}
// Delete message from table.
tableModel.deleteMessage(table.getSelectedRow());
// Update GUI.
messageTextArea.setText(«»);
deleting = false;
selectedMessage = null;
updateButtons();
}
// Send the specified message.
private void sendMessage(int type, Message message) {
// Display message dialog to get message values.
MessageDialog dialog;
try {
dialog = new MessageDialog(this, type, message);
if (!dialog.display()) {
// Return if dialog was cancelled.
return;
}
} catch (Exception e) {
showError(«Unable to send message.», false);
return;
}
try {
// Create a new message with values from dialog.
Message newMessage = new MimeMessage(session);
newMessage.setFrom(new InternetAddress(dialog.getFrom()));
newMessage.setRecipient(Message.RecipientType.TO,
new InternetAddress(dialog.getTo()));
newMessage.setSubject(dialog.getSubject());
newMessage.setSentDate(new Date());
newMessage.setText(dialog.getContent());
// Send new message.
Transport.send(newMessage);
} catch (Exception e) {
showError(«Unable to send message.», false);
}
}
// Show the selected message in the content panel.
private void showSelectedMessage() {
// Show hour glass cursor while message is loaded.
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
try {
messageTextArea.setText(
getMessageContent(selectedMessage));
messageTextArea.setCaretPosition(0);
} catch (Exception e) {
showError(«Unabled to load message.», false);
} finally {
// Return to default cursor.
setCursor(Cursor.getDefaultCursor());
}
}
/* Update each button’s state based off of whether or not
there is a message currently selected in the table. */
private void updateButtons() {
if (selectedMessage != null) {
replyButton.setEnabled(true);
forwardButton.setEnabled(true);
deleteButton.setEnabled(true);
} else {
replyButton.setEnabled(false);
forwardButton.setEnabled(false);
deleteButton.setEnabled(false);
}
}
// Show the application window on the screen.
public void show() {
super.show();
// Update the split panel to be divided 50/50.
splitPane.setDividerLocation(.5);
}
// Connect to e-mail server.
public void connect() {
// Display connect dialog.
ConnectDialog dialog = new ConnectDialog(this);
dialog.show();
// Build connection URL from connect dialog settings.
StringBuffer connectionUrl = new StringBuffer();
connectionUrl.append(dialog.getType() + «://»);
connectionUrl.append(dialog.getUsername() + «:»);
connectionUrl.append(dialog.getPassword() + «@»);
connectionUrl.append(dialog.getServer() + «/»);
/* Display dialog stating that messages are
currently being downloaded from server. */
final DownloadingDialog downloadingDialog =
new DownloadingDialog(this);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
downloadingDialog.show();
}
});
// Establish JavaMail session and connect to server.
Store store = null;
try {
// Initialize JavaMail session with SMTP server.
Properties props = new Properties();
props.put(«mail.smtp.host», dialog.getSmtpServer());
session = Session.getDefaultInstance(props, null);
// Connect to e-mail server.
URLName urln = new URLName(connectionUrl.toString());
store = session.getStore(urln);
store.connect();
} catch (Exception e) {
// Close the downloading dialog.
downloadingDialog.dispose();
// Show error dialog.
showError(«Unable to connect.», true);
}
// Download message headers from server.
try {
// Open main «INBOX» folder.
Folder folder = store.getFolder(«INBOX»);
folder.open(Folder.READ_WRITE);
// Get folder’s list of messages.
Message[] messages = folder.getMessages();
// Retrieve message headers for each message in folder.
FetchProfile profile = new FetchProfile();
profile.add(FetchProfile.Item.ENVELOPE);
folder.fetch(messages, profile);
// Put messages in table.
tableModel.setMessages(messages);
} catch (Exception e) {
// Close the downloading dialog.
downloadingDialog.dispose();
// Show error dialog.
showError(«Unable to download messages.», true);
}
// Close the downloading dialog.
downloadingDialog.dispose();
}
// Show error dialog and exit afterwards if necessary.
private void showError(String message, boolean exit) {
JOptionPane.showMessageDialog(this, message, «Error»,
JOptionPane.ERROR_MESSAGE);
if (exit)
System.exit(0);
}
// Get a message’s content.
public static String getMessageContent(Message message)
throws Exception {
Object content = message.getContent();
if (content instanceof Multipart) {
StringBuffer messageContent = new StringBuffer();
Multipart multipart = (Multipart) content;
for (int i = 0; i < multipart.getCount(); i++) {
Part part = (Part) multipart.getBodyPart(i);
if (part.isMimeType(«text/plain»)) {
messageContent.append(part.getContent().toString());
}
}
return messageContent.toString();
} else {
return content.toString();
}
}
// Run the E-mail Client.
public static void main(String[] args) {
EmailClient client = new EmailClient();
client.show();
// Display connect dialog.
client.connect();
}
}
ConnectDialog:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/* This class displays a dialog for entering e-mail
server connection settings. */
public class ConnectDialog extends JDialog {
// These are the e-mail server types.
private static final String[] TYPES = {«pop3», «imap»};
// Combo box for e-mail server types.
private JComboBox typeComboBox;
// Server, username and SMTP server text fields.
private JTextField serverTextField, usernameTextField;
private JTextField smtpServerTextField;
// Password text field.
private JPasswordField passwordField;
// Constructor for dialog.
public ConnectDialog(Frame parent) {
// Call super constructor, specifying that dialog is modal.
super(parent, true);
// Set dialog title.
setTitle(«Connect»);
// Handle closing events.
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
actionCancel();
}
});
// Setup settings panel.
JPanel settingsPanel = new JPanel();
settingsPanel.setBorder(
BorderFactory.createTitledBorder(«Connection Settings»));
GridBagConstraints constraints;
GridBagLayout layout = new GridBagLayout();
settingsPanel.setLayout(layout);
JLabel typeLabel = new JLabel(«Type:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(typeLabel, constraints);
settingsPanel.add(typeLabel);
typeComboBox = new JComboBox(TYPES);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.WEST;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 0, 5);
constraints.weightx = 1.0D;
layout.setConstraints(typeComboBox, constraints);
settingsPanel.add(typeComboBox);
JLabel serverLabel = new JLabel(«Server:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(serverLabel, constraints);
settingsPanel.add(serverLabel);
serverTextField = new JTextField(25);
constraints = new GridBagConstraints();
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 0, 5);
constraints.weightx = 1.0D;
layout.setConstraints(serverTextField, constraints);
settingsPanel.add(serverTextField);
JLabel usernameLabel = new JLabel(«Username:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(usernameLabel, constraints);
settingsPanel.add(usernameLabel);
usernameTextField = new JTextField();
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.WEST;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 0, 5);
constraints.weightx = 1.0D;
layout.setConstraints(usernameTextField, constraints);
settingsPanel.add(usernameTextField);
JLabel passwordLabel = new JLabel(«Password:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 5, 0);
layout.setConstraints(passwordLabel, constraints);
settingsPanel.add(passwordLabel);
passwordField = new JPasswordField();
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.WEST;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 5, 5);
constraints.weightx = 1.0D;
layout.setConstraints(passwordField, constraints);
settingsPanel.add(passwordField);
JLabel smtpServerLabel = new JLabel(«SMTP Server:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 5, 0);
layout.setConstraints(smtpServerLabel, constraints);
settingsPanel.add(smtpServerLabel);
smtpServerTextField = new JTextField(25);
constraints = new GridBagConstraints();
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 5, 5);
constraints.weightx = 1.0D;
layout.setConstraints(smtpServerTextField, constraints);
settingsPanel.add(smtpServerTextField);
// Setup buttons panel.
JPanel buttonsPanel = new JPanel();
JButton connectButton = new JButton(«Connect»);
connectButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionConnect();
}
});
buttonsPanel.add(connectButton);
JButton cancelButton = new JButton(«Cancel»);
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionCancel();
}
});
buttonsPanel.add(cancelButton);
// Add panels to display.
getContentPane().setLayout(new BorderLayout());
getContentPane().add(settingsPanel, BorderLayout.CENTER);
getContentPane().add(buttonsPanel, BorderLayout.SOUTH);
// Size dialog to components.
pack();
// Center dialog over application.
setLocationRelativeTo(parent);
}
// Validate connection settings and close dialog.
private void actionConnect() {
if (serverTextField.getText().trim().length() < 1
|| usernameTextField.getText().trim().length() < 1
|| passwordField.getPassword().length < 1
|| smtpServerTextField.getText().trim().length() < 1) {
JOptionPane.showMessageDialog(this,
«One or more settings is missing.»,
«Missing Setting(s)», JOptionPane.ERROR_MESSAGE);
return;
}
// Close dialog.
dispose();
}
// Cancel connecting and exit program.
private void actionCancel() {
System.exit(0);
}
// Get e-mail server type.
public String getType() {
return (String) typeComboBox.getSelectedItem();
}
// Get e-mail server.
public String getServer() {
return serverTextField.getText();
}
// Get e-mail username.
public String getUsername() {
return usernameTextField.getText();
}
// Get e-mail password.
public String getPassword() {
return new String(passwordField.getPassword());
}
// Get e-mail SMTP server.
public String getSmtpServer() {
return smtpServerTextField.getText();
}
}
MessagesTableModel:
import java.util.*;
import javax.mail.*;
import javax.swing.*;
import javax.swing.table.*;
// This class manages the e-mail table’s data.
public class MessagesTableModel extends AbstractTableModel {
// These are the names for the table’s columns.
private static final String[] columnNames = {«Sender»,
«Subject», «Date»};
// The table’s list of messages.
private ArrayList messageList = new ArrayList();
// Sets the table’s list of messages.
public void setMessages(Message[] messages) {
for (int i = messages.length — 1; i >= 0; i—) {
messageList.add(messages[i]);
}
// Fire table data change notification to table.
fireTableDataChanged();
}
// Get a message for the specified row.
public Message getMessage(int row) {
return (Message) messageList.get(row);
}
// Remove a message from the list.
public void deleteMessage(int row) {
messageList.remove(row);
// Fire table row deletion notification to table.
fireTableRowsDeleted(row, row);
}
// Get table’s column count.
public int getColumnCount() {
return columnNames.length;
}
// Get a column’s name.
public String getColumnName(int col) {
return columnNames[col];
}
// Get table’s row count.
public int getRowCount() {
return messageList.size();
}
// Get value for a specific row and column combination.
public Object getValueAt(int row, int col) {
try {
Message message = (Message) messageList.get(row);
switch (col) {
case 0: // Sender
Address[] senders = message.getFrom();
if (senders != null || senders.length > 0) {
return senders[0].toString();
} else {
return «[none]»;
}
case 1: // Subject
String subject = message.getSubject();
if (subject != null && subject.length() > 0) {
return subject;
} else {
return «[none]»;
}
case 2: // Date
Date date = message.getSentDate();
if (date != null) {
return date.toString();
} else {
return «[none]»;
}
}
} catch (Exception e) {
// Fail silently.
return «»;
}
return «»;
}
}
MessageDialog:
import java.awt.*;
import java.awt.event.*;
import javax.mail.*;
import javax.swing.*;
// This class displays the dialog used for creating messages.
public class MessageDialog extends JDialog {
// Dialog message identifiers.
public static final int NEW = 0;
public static final int REPLY = 1;
public static final int FORWARD = 2;
// Message from, to and subject text fields.
private JTextField fromTextField, toTextField;
private JTextField subjectTextField;
// Message content text area.
private JTextArea contentTextArea;
// Flag specifying whether or not dialog was cancelled.
private boolean cancelled;
// Constructor for dialog.
public MessageDialog(Frame parent, int type, Message message)
throws Exception {
// Call super constructor, specifying that dialog is modal.
super(parent, true);
/* Set dialog title and get message’s «to», «subject»
and «content» values based on message type. */
String to = «», subject = «», content = «»;
switch (type) {
// Reply message.
case REPLY:
setTitle(«Reply To Message»);
// Get message «to» value
Address[] senders = message.getFrom();
if (senders != null || senders.length > 0) {
to = senders[0].toString();
}
to = message.getFrom()[0].toString();
// Get message subject.
subject = message.getSubject();
if (subject != null && subject.length() > 0) {
subject = «RE: « + subject;
} else {
subject = «RE:»;
}
// Get message content and add «REPLIED TO» notation.
content = «n—————— « +
«REPLIED TO MESSAGE» +
» ——————n» +
EmailClient.getMessageContent(message);
break;
// Forward message.
case FORWARD:
setTitle(«Forward Message»);
// Get message subject.
subject = message.getSubject();
if (subject != null && subject.length() > 0) {
subject = «FWD: « + subject;
} else {
subject = «FWD:»;
}
// Get message content and add «FORWARDED» notation.
content = «n—————— « +
«FORWARDED MESSAGE» +
» ——————n» +
EmailClient.getMessageContent(message);
break;
// New message.
default:
setTitle(«New Message»);
}
// Handle closing events.
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
actionCancel();
}
});
// Setup fields panel.
JPanel fieldsPanel = new JPanel();
GridBagConstraints constraints;
GridBagLayout layout = new GridBagLayout();
fieldsPanel.setLayout(layout);
JLabel fromLabel = new JLabel(«From:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(fromLabel, constraints);
fieldsPanel.add(fromLabel);
fromTextField = new JTextField();
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(fromTextField, constraints);
fieldsPanel.add(fromTextField);
JLabel toLabel = new JLabel(«To:»);
constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.EAST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(toLabel, constraints);
fieldsPanel.add(toLabel);
toTextField = new JTextField(to);
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 0, 0);
constraints.weightx = 1.0D;
layout.setConstraints(toTextField, constraints);
fieldsPanel.add(toTextField);
JLabel subjectLabel = new JLabel(«Subject:»);
constraints = new GridBagConstraints();
constraints.insets = new Insets(5, 5, 5, 0);
layout.setConstraints(subjectLabel, constraints);
fieldsPanel.add(subjectLabel);
subjectTextField = new JTextField(subject);
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 5, 0);
layout.setConstraints(subjectTextField, constraints);
fieldsPanel.add(subjectTextField);
// Setup content panel.
JScrollPane contentPanel = new JScrollPane();
contentTextArea = new JTextArea(content, 10, 50);
contentPanel.setViewportView(contentTextArea);
// Setup buttons panel.
JPanel buttonsPanel = new JPanel();
JButton sendButton = new JButton(«Send»);
sendButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionSend();
}
});
buttonsPanel.add(sendButton);
JButton cancelButton = new JButton(«Cancel»);
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
actionCancel();
}
});
buttonsPanel.add(cancelButton);
// Add panels to display.
getContentPane().setLayout(new BorderLayout());
getContentPane().add(fieldsPanel, BorderLayout.NORTH);
getContentPane().add(contentPanel, BorderLayout.CENTER);
getContentPane().add(buttonsPanel, BorderLayout.SOUTH);
// Size dialog to components.
pack();
// Center dialog over application.
setLocationRelativeTo(parent);
}
// Validate message fields and close dialog.
private void actionSend() {
if (fromTextField.getText().trim().length() < 1
|| toTextField.getText().trim().length() < 1
|| subjectTextField.getText().trim().length() < 1
|| contentTextArea.getText().trim().length() < 1) {
JOptionPane.showMessageDialog(this,
«One or more fields is missing.»,
«Missing Field(s)», JOptionPane.ERROR_MESSAGE);
return;
}
// Close dialog.
dispose();
}
// Cancel creating this message and close dialog.
private void actionCancel() {
cancelled = true;
// Close dialog.
dispose();
}
// Show dialog.
public boolean display() {
show();
// Return whether or not display was successful.
return !cancelled;
}
// Get message’s «From» field value.
public String getFrom() {
return fromTextField.getText();
}
// Get message’s «To» field value.
public String getTo() {
return toTextField.getText();
}
// Get message’s «Subject» field value.
public String getSubject() {
return subjectTextField.getText();
}
// Get message’s «content» field value.
public String getContent() {
return contentTextArea.getText();
}
}
DownloadingDialog:
import java.awt.*;
import javax.swing.*;
/* This class displays a simple dialog instructing the user
that messages are being downloaded. */
public class DownloadingDialog extends JDialog {
// Constructor for dialog.
public DownloadingDialog(Frame parent) {
// Call super constructor, specifying that dialog is modal.
super(parent, true);
// Set dialog title.
setTitle(«E-mail Client»);
// Instruct window not to close when the «X» is clicked.
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
// Put a message with a nice border in this dialog.
JPanel contentPane = new JPanel();
contentPane.setBorder(
BorderFactory.createEmptyBorder(5, 5, 5, 5));
contentPane.add(new JLabel(«Downloading messages…»));
setContentPane(contentPane);
// Size dialog to components.
pack();
// Center dialog over application.
setLocationRelativeTo(parent);
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 |
/********************************************************************* MODULE NAME: b64.c AUTHOR: Bob Trower 08/04/01 PROJECT: Crypt Data Packaging COPYRIGHT: Copyright (c) Trantor Standard Systems Inc., 2001 NOTE: This source code may be used as you wish, subject to the MIT license. See the LICENCE section below. DESCRIPTION: This little utility implements the Base64 Content-Transfer-Encoding standard described in RFC1113 ([url]http://www.faqs.org/rfcs/rfc1113.html[/url]). This is the coding scheme used by MIME to allow binary data to be transferred by SMTP mail. Groups of 3 bytes from a binary stream are coded as groups of 4 bytes in a text stream. The input stream is 'padded' with zeros to create an input that is an even multiple of 3. A special character ('=') is used to denote padding so that the stream can be decoded back to its exact size. Encoded output is formatted in lines which should be a maximum of 72 characters to conform to the specification. This program defaults to 72 characters, but will allow more or less through the use of a switch. The program enforces a minimum line size of 4 characters. Example encoding: The stream 'ABCD' is 32 bits long. It is mapped as follows: ABCD A (65) B (66) C (67) D (68) (None) (None) 01000001 01000010 01000011 01000100 16 (Q) 20 (U) 9 (J) 3 (D) 17 (R) 0 (A) NA (=) NA (=) 010000 010100 001001 000011 010001 000000 000000 000000 QUJDRA== Decoding is the process in reverse. A 'decode' lookup table has been created to avoid string scans. DESIGN GOALS: Specifically: Code is a stand-alone utility to perform base64 encoding/decoding. It should be genuinely useful when the need arises and it meets a need that is likely to occur for some users. Code acts as sample code to show the author's design and coding style. Generally: This program is designed to survive: Everything you need is in a single source file. It compiles cleanly using a vanilla ANSI C compiler. It does its job correctly with a minimum of fuss. The code is not overly clever, not overly simplistic and not overly verbose. Access is 'cut and paste' from a web page. Terms of use are reasonable. VALIDATION: Non-trivial code is never without errors. This file likely has some problems, since it has only been tested by the author. It is expected with most source code that there is a period of 'burn-in' when problems are identified and corrected. That being said, it is possible to have 'reasonably correct' code by following a regime of unit test that covers the most likely cases and regression testing prior to release. This has been done with this code and it has a good probability of performing as expected. Unit Test Cases: case 0:empty file: CASE0.DAT -> -> (Zero length target file created on both encode and decode.) case 1:One input character: CASE1.DAT A -> QQ== -> A case 2:Two input characters: CASE2.DAT AB -> QUJD -> AB case 3:Three input characters: CASE3.DAT ABC -> QUJD -> ABC case 4:Four input characters: case4.dat ABCD -> QUJDRA== -> ABCD case 5:All chars from 0 to ff, linesize set to 50: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIj JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZH SElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6P kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKz tLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX 2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7 /P3+/w== case 6:Mime Block from e-mail: (Data same as test case 5) case 7: Large files: Tested 28 MB file in/out. case 8: Random Binary Integrity: This binary program (b64.exe) was encoded to base64, back to binary and then executed. case 9 Stress: All files in a working directory encoded/decoded and compared with file comparison utility to ensure that multiple runs do not cause problems such as exhausting file handles, tmp storage, etc. ------------- Syntax, operation and failure: All options/switches tested. Performs as expected. case 10: No Args -- Shows Usage Screen Return Code 1 (Invalid Syntax) case 11: One Arg (invalid) -- Shows Usage Screen Return Code 1 (Invalid Syntax) case 12: One Arg Help (-?) -- Shows detailed Usage Screen. Return Code 0 (Success -- help request is valid). case 13: One Arg Help (-h) -- Shows detailed Usage Screen. Return Code 0 (Success -- help request is valid). case 14: One Arg (valid) -- Uses stdin/stdout (filter) Return Code 0 (Sucess) case 15: Two Args (invalid file) -- shows system error. Return Code 2 (File Error) case 16: Encode non-existent file -- shows system error. Return Code 2 (File Error) case 17: Out of disk space -- shows system error. Return Code 3 (File I/O Error) ------------- Compile/Regression test: gcc compiled binary under Cygwin Microsoft Visual Studio under Windows 2000 Microsoft Version 6.0 C under Windows 2000 DEPENDENCIES: None LICENCE: Copyright (c) 2001 Bob Trower, Trantor Standard Systems Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. VERSION HISTORY: Bob Trower 08/04/01 -- Create Version 0.00.00B ******************************************************************* */ #include <stdio.h> #include <stdlib.h> /* ** Translation Table as described in RFC1113 */ static const char cb64[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /* ** Translation Table to decode (created by author) */ static const char cd64[]="|$$$}rstuvwxyz{$$$$$$$>?@ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ[\]^_`abcdefghijklmnopq"; /* ** encodeblock ** ** encode 3 8-bit binary bytes as 4 '6-bit' characters */ void encodeblock( unsigned char in[3], unsigned char out[4], int len ) { out[0] = cb64[ in[0] >> 2 ]; out[1] = cb64[ ((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4) ]; out[2] = (unsigned char) (len > 1 ? cb64[ ((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6) ] : '='); out[3] = (unsigned char) (len > 2 ? cb64[ in[2] & 0x3f ] : '='); } /* ** encode ** ** base64 encode a stream adding padding and line breaks as per spec. */ void encode( FILE *infile, FILE *outfile, int linesize ) { unsigned char in[3], out[4]; int i, len, blocksout = 0; while( !feof( infile ) ) { len = 0; for( i = 0; i < 3; i++ ) { in[i] = (unsigned char) getc( infile ); if( !feof( infile ) ) { len++; } else { in[i] = 0; } } if( len ) { encodeblock( in, out, len ); for( i = 0; i < 4; i++ ) { putc( out[i], outfile ); } blocksout++; } if( blocksout >= (linesize/4) || feof( infile ) ) { if( blocksout ) { fprintf( outfile, "rn" ); } blocksout = 0; } } } /* ** decodeblock ** ** decode 4 '6-bit' characters into 3 8-bit binary bytes */ void decodeblock( unsigned char in[4], unsigned char out[3] ) { out[ 0 ] = (unsigned char ) (in[0] << 2 | in[1] >> 4); out[ 1 ] = (unsigned char ) (in[1] << 4 | in[2] >> 2); out[ 2 ] = (unsigned char ) (((in[2] << 6) & 0xc0) | in[3]); } /* ** decode ** ** decode a base64 encoded stream discarding padding, line breaks and noise */ void decode( FILE *infile, FILE *outfile ) { unsigned char in[4], out[3], v; int i, len; while( !feof( infile ) ) { for( len = 0, i = 0; i < 4 && !feof( infile ); i++ ) { v = 0; while( !feof( infile ) && v == 0 ) { v = (unsigned char) getc( infile ); v = (unsigned char) ((v < 43 || v > 122) ? 0 : cd64[ v - 43 ]); if( v ) { v = (unsigned char) ((v == '$') ? 0 : v - 61); } } if( !feof( infile ) ) { len++; if( v ) { in[ i ] = (unsigned char) (v - 1); } } else { in[i] = 0; } } if( len ) { decodeblock( in, out ); for( i = 0; i < len - 1; i++ ) { putc( out[i], outfile ); } } } } /* ** returnable errors ** ** Error codes returned to the operating system. ** */ #define B64_SYNTAX_ERROR 1 #define B64_FILE_ERROR 2 #define B64_FILE_IO_ERROR 3 #define B64_ERROR_OUT_CLOSE 4 #define B64_LINE_SIZE_TO_MIN 5 /* ** b64_message ** ** Gather text messages in one place. ** */ char *b64_message( int errcode ) { #define B64_MAX_MESSAGES 6 char *msgs[ B64_MAX_MESSAGES ] = { "b64:000:Invalid Message Code.", "b64:001:Syntax Error -- check help for usage.", "b64:002:File Error Opening/Creating Files.", "b64:003:File I/O Error -- Note: output file not removed.", "b64:004:Error on output file close.", "b64:004:linesize set to minimum." }; char *msg = msgs[ 0 ]; if( errcode > 0 && errcode < B64_MAX_MESSAGES ) { msg = msgs[ errcode ]; } return( msg ); } /* ** b64 ** ** 'engine' that opens streams and calls encode/decode */ int b64( int opt, char *infilename, char *outfilename, int linesize ) { FILE *infile; int retcode = B64_FILE_ERROR; if( !infilename ) { infile = stdin; } else { infile = fopen( infilename, "rb" ); } if( !infile ) { perror( infilename ); } else { FILE *outfile; if( !outfilename ) { outfile = stdout; } else { outfile = fopen( outfilename, "wb" ); } if( !outfile ) { perror( outfilename ); } else { if( opt == 'e' ) { encode( infile, outfile, linesize ); } else { decode( infile, outfile ); } if (ferror( infile ) || ferror( outfile )) { retcode = B64_FILE_IO_ERROR; } else { retcode = 0; } if( outfile != stdout ) { if( fclose( outfile ) != 0 ) { perror( b64_message( B64_ERROR_OUT_CLOSE ) ); retcode = B64_FILE_IO_ERROR; } } } if( infile != stdin ) { fclose( infile ); } } return( retcode ); } /* ** showuse ** ** display usage information, help, version info */ void showuse( int morehelp ) { { printf( "n" ); printf( " b64 (Base64 Encode/Decode) Bob Trower 08/03/01 n" ); printf( " (C) Copr Bob Trower 1986-01. Version 0.00B n" ); printf( " Usage: b64 -option [ -l num ] [<FileIn> [<FileOut>]] n" ); printf( " Purpose: This program is a simple utility that implementsn" ); printf( " Base64 Content-Transfer-Encoding (RFC1113). n" ); } if( !morehelp ) { printf( " Use -h option for additional help. n" ); } else { printf( " Options: -e encode to Base64 -h This help text. n" ); printf( " -d decode from Base64 -? This help text. n" ); printf( " Note: -l use to change line size (from 72 characters)n" ); printf( " Returns: 0 = success. Non-zero is an error code. n" ); printf( " ErrCode: 1 = Bad Syntax, 2 = File Open, 3 = File I/O n" ); printf( " Example: b64 -e binfile b64file <- Encode to b64 n" ); printf( " b64 -d b64file binfile <- Decode from b64 n" ); printf( " b64 -e -l40 infile outfile <- Line Length of 40 n" ); printf( " Note: Will act as a filter, but this should only be n" ); printf( " used on text files due to translations made by n" ); printf( " operating systems. n" ); printf( " Release: 0.00.00, Tue Aug 7 2:00:00 2001, ANSI-SOURCE Cn" ); } } #define B64_DEF_LINE_SIZE 72 #define B64_MIN_LINE_SIZE 4 #define THIS_OPT(ac, av) (ac > 1 ? av[1][0] == '-' ? av[1][1] : 0 : 0) /* ** main ** ** parse and validate arguments and call b64 engine or help */ int main( int argc, char **argv ) { int opt = 0; int retcode = 0; int linesize = B64_DEF_LINE_SIZE; char *infilename = NULL, *outfilename = NULL; while( THIS_OPT( argc, argv ) ) { switch( THIS_OPT(argc, argv) ) { case 'l': linesize = atoi( &(argv[1][2]) ); if( linesize < B64_MIN_LINE_SIZE ) { linesize = B64_MIN_LINE_SIZE; printf( "%sn", b64_message( B64_LINE_SIZE_TO_MIN ) ); } break; case '?': case 'h': opt = 'h'; break; case 'e': case 'd': opt = THIS_OPT(argc, argv); break; default: opt = 0; break; } argv++; argc--; } switch( opt ) { case 'e': case 'd': infilename = argc > 1 ? argv[1] : NULL; outfilename = argc > 2 ? argv[2] : NULL; retcode = b64( opt, infilename, outfilename, linesize ); break; case 0: retcode = B64_SYNTAX_ERROR; case 'h': showuse( opt ); break; } if( retcode ) { printf( "%sn", b64_message( retcode ) ); } return( retcode ); } |