В PHP отправка почты реализована одной строкой кода! Java требует 3 недели??! (из разговоров с разработчиками и менеджерами) |
Итак, задача — реализовать сервис отправки электронной почты (войны).
Этапы разработки:
Начнем с самой отправки.
Если не Spring (для небольшого модуля он не нужен), подключите apache commons-email
и написать<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-email</artifactId> <version>1.3</version> </dependency>
public class MailSender {
public static void sendMail {
HtmlEmail email = .
?
email.send();
Простите, а где взять настройки почтового сервера? Не думаю, что младшему разработчику даже придет в голову их жестко закодировать, поэтому:
Конфигурация почтового сервера
Будем надеяться, что общую работу по настройке всей системы мы уже проделали, осталось только реализовать ее для почтового модуля: .
<part key="email">
<entry key="hostName">smtp.gmail.com</entry>
<entry key="user">[email protected]</entry>
<entry key="password">psw</entry>
<entry key="smtpPort">465</entry>
<entry key="useSSL">true</entry>
<entry key="debug">false</entry>
<entry key="charset">UTF-8</entry>
<entry key="useTLS">false</entry>
</part>
public class MailConfig {
public static <T extends Email> T prepareEmail(T email) {
email.setHostName(hostName);
email.setSmtpPort(port);
email.setSSL(useSSL);
email.setTLS(useTLS);
email.setDebug(debug);
email.setAuthenticator(defaultAuthenticator);
email.setCharset(charset);
return email;
}
Сервисный звонок
Да, у нас есть сервис – как мы хотим его назвать? Бизнесу нужна интеграция с веб-сервисами, и ему также необходимо отправлять через простой HTTP GET (например, вызывать прямо из браузера):- Отправка через HTTP GET:
public class MailServlet extends CommonServlet { @Override protected void doProcess(HttpServletRequest request, HttpServletResponse response, Map<String, String> params) throws IOException, ServletException { String from = ConfigUtil.getProperty("from", params); .
MailSender.sendMail(from, to, cc, .
);
- Реализация веб-сервиса (JAX-WS) сложнее:
@WebService @SOAPBinding(style = Style.RPC) public interface MailService { @WebMethod public void sendMail( @WebParam(name = "from") String from, @WebParam(name = "to") String to, @WebService(endpointInterface = "mycompany.MailService") public class MailServiceImpl implements MailService { @Override public void sendMail(String from, String to, String cc, String subject, String body, String attachmentUrls) throws StateException { MailSender.sendMailAndRecordHistory(from, to, cc, subject, body, .
); }
и mailService.wsdl:<definitions .
targetNamespace=" http://mail.mycompany.com/ " name="MailServiceImplService"> <message name="sendMail"> <part name="from" type=" xsd:string"/ > .
<portType name="MailService"> <operation name="sendMail" parameterOrder="from to cc subject body attachmentUrls"> <input wsam:Action="http://mail.mycompany.com/MailService/sendMailRequest " message=" tns:sendMail"/ > .
<binding name="MailServiceImplPortBinding" type=" tns:MailService "> < soap:binding transport=" http://schemas.xmlsoap.org/soap/http " style="rpc"/> <operation name="sendMail"> < soap:operation soapAction=""/> .
<service name="MailServiceImplService"> <port name="MailServiceImplPort" binding=" tns:MailServiceImplPortBinding "> < soap:address location=" http://mycompany:8080/mail/mailService"/ > .
Не забудьте web.xml (Tomcat)<listener> <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class> </listener> <servlet> <servlet-name>mailService</servlet-name> <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>mailService</servlet-name> <url-pattern>/mailService</url-pattern> </servlet-mapping> <servlet> <servlet-name>mailServlet</servlet-name> <servlet-class>com.mycompany.mail.MailServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> .
Распределение почтовых клиентов
А как же соседнему модулю нашей системы быстро подтянуть наш сервис через веб-сервис? Самый простой способ — выбрать модуль maven mail-client, сделать нашу почтовую службу зависимой от него и разрешить любому модулю — нашему клиенту включать (зависимость maven) mail-client:- Делаем отдельный maven модуль mail-client и помещаем в него mailService.wsdl и интерфейс MailService
<groupId>com.mycompany</groupId> <artifactId>mail-client</artifactId> <name>Mail Client</name>
- Кроме того, для полной радости нашего внутреннего клиента, мы делаем MailWSClient:
вызвать соседний модуль будет довольно просто:
MailWSClient.sendMail(.
public class MailWSClient { static String mailWsdl; private static final Service SERVICE; static { URL url = MailWSClient.class.getClassLoader().
getResource("mailService.wsdl"); SERVICE = Service.create(url, new QName(" http://mail.mycompany.com/ ", "MailServiceImplService")); // get mail endpoint from config mailWsdl = Config.getUrlAsString("mail/mailServiceЭwsdl"); } public static void sendMail(String from, String to, .
){ getPort().
sendMail(from, .
private static MailService getPort() { MailService port = SERVICE.getPort(MailService.class); Map<String, Object> requestContext = ((BindingProvider) port).
getRequestContext(); requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, mailWsdl); return port; }
Прикручиваем шаблоны
Эге.В модуле документооборота у нас 52 вида документов.
Было бы неплохо предоставить нашим клиентам возможность самим определять шаблон письма.
Более того, у нас уже реализован такой сервис (TemplateService).
Сервис шаблонов прост: он реализован на jsp, ему передаются ключ и параметры с помощью get, а возвращается готовый текст.
- Добавьте sendTemplateMail в MailService, MailWSClient, MailServiceImpl, MailSender, mailService.wsdl и MailServlet:
sendTemplateMail(.
, templateKey, params);
- И реализуем это в MailSender (удобная обертка MyHttpConnection у нас уже есть, реализованная через HttpURLConnection.openConnection())
static void sendTemplateMail(.
, String key, String params) { LOGGER.info("Send template mail from .
String templateUrl = getUrlAsString("templateЭtype=mail&format=html&key=" + key .
MyHttpConnection conn = MyHttpConnection.connect(templateUrl, params); if (conn.isOk()) { String body = conn.getMsg(); sendMail(from, to, cc, MailUtil.getSubject(body), body); } else { throw LOGGER.getStateException(conn.toString(), ExceptionType.TEMPLATE); .
Попутно пришлось решить проблему с темой: сервис шаблонов возвращает только тело письма.Шаблон возвращается в формате html, MailUtil извлекает заголовок из шаблона и использует его в качестве темы:
public class MailUtil { static Pattern MAIL_TITLE = Pattern.compile("<title>(.
+)</title>", Pattern.MULTILINE); static String getSubject(String template) { Matcher m = MAIL_TITLE.matcher(template); return m.find() ? m.group(1) : null; }
Отправка документа
На самом деле у нас есть документы.Что, если мы вызовем наш сервис с идентификатором документа? В TemplateService уже есть шаблоны документов.
- Добавьте sendDocMail в MailService, MailWSClient, MailServiceImpl, MailSender, mailService.wsdl и MailServlet.
sendDocMail(String from, String to, String cc, String key, long docId);
- Упс, к документам есть вложения, которые нужно приложить к письму.
К счастью, commons-email позволяет это легко сделать, и у нас есть общий модуль maven Attach-common, из которого можно запросить список вложений по docId:
public class MailSender { static void sendDocMail(String from, String to, String cc, String key, long docId) throws StateException { List<Attach> list = AttachUtil.getList(docId); MailSender.sendTemplateMailAndRecordHistory(from, to, cc, key, "objectid=" + docId, MailUtil.formatAttach(list)); } public class MailUtil { // format attaches as // ulr1[name1], ulr2[name2], .
static String formatAttach(List<Attach> list) { return Util.collectionToDelimitedString(list, new Presentable<Attach>() { @Override public String toString(Attach attach) { return AttachConfig.downloadUrl + attach.getUuid() + '[' + attach.getName() + ']'; }
Отказоустойчивость
Что делать, если сервер временно недоступен? Нам нужно сохранить историю в базе и сделать пушер.Заодно решим проблему отправки письма пользователю, который поставил ему задачу из BPM - это можно реализовать через триггер в базе данных: вставьте в таблицу строку TODO. В качестве побочного эффекта у нас есть история отправки наших сообщений, затем мы можем добавить пользовательский интерфейс сверху и просто выполнять SQL-запросы к таблице.
Хорошо, что механизм сканирования у нас уже есть — просто нужна еще одна его реализация.
- Создайте таблицу mail_action в базе данных.
CREATE TABLE hist.mail_action ( id SERIAL, _from TEXT, _to TEXT NOT NULL, _cc TEXT, subject TEXT, body TEXT, attachmenturls TEXT, state TEXT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE, key reference.ui_key, params TEXT );
- Добавление интервалов сканирования в конфигурацию
<entry key="scanTodoInterval">30</entry> <entry key="scanFailInterval">600</entry>
scanTodoInterval = ConfigUtil.getInt(SCAN_TODO_INTERVAL, mailProps, 60); // default 60 sec scanFailInterval = ConfigUtil.getInt(SCAN_FAIL_INTERVAL, mailProps, 600); // default 10 min
Реализуем в MailSender запись истории отправки в базу данных вместе со статусом (ОК или Exception).Сканируем таблицу mail_action и на основании состояния (TODO, EmailException) отправляем письмо
<listener> <listener-class>com.mycompany.common.web.SchedulerListener</listener-class> </listener>
public class MailWebScanner implements WebScheduler { private final MailScanner todoScanner = new MailScanner("TODO"); private final MailScanner failScanner = new MailScanner("org.apache.commons.mail.EmailException"); @Override public void activate(ServletContext servletContext) { todoScanner.startScanning(MailConfig.scanTodoInterval); failScanner.startScanning(MailConfig.scanFailInterval); } @Override public void deactivate() { todoScanner.deactivate(); failScanner.deactivate(); } @Override public void shutdown() { AsyncExecutor.shutdown(); } } public class MailScanner extends Scanner { private static final BeanListHandler<MailBean> HANDLER = new BeanListHandler<MailBean>(MailBean.class); private final String startWith; public MailScanner(String startWith) { this.startWith = startWith; } void startScanning(int interval) { activate(new Runnable() { @Override public void run() { for (MailBean mail : getMailToSend()) { MailSender.sendTemplateMailAndRecordHistory( } } }, interval, false); } .
List<MailBean> getMailToSend() { return SqlUtil.executeQuery("select * from hist.mail_action where state like '" + startWith + "%'", HANDLER); .
Для тех, кто не любит ждать: асинхронность
Поскольку наш сервис теперь отказоустойчив, мы предоставим клиентам наших веб-сервисов возможность не ждать ответа.
Вместо того, чтобы дублировать все методы службы с постфиксом Async и аннотацией @OneWay, давайте добавим флаг async и вызов AsyncExecutor (наша оболочка поверх ScheduledThreadPoolExecutor) к вызовам MailWSClient: public class MailWSClient {
public static void sendMail(final String from, final String to, final String cc, final String subject, final String body, final String attachmentUrls, boolean async) throws StateException {
send(new Runnable() {
@Override
public void run() {
getPort().
sendMail(mask(from), mask(to), mask(cc), mask(subject), mask(body), mask(attachmentUrls)); } }, async); } public static void sendTemplateMail(final String from, final String to, final String cc, final String key, final String params, final String attachmentUrls, boolean async) throws StateException { .
public static void sendDocMail(final String from, final String to, final String cc, final String key, final long docId, boolean async) throws StateException { .
private static void send(Runnable task, boolean async) {
if (async) {
AsyncExecutor.submit(task);
} else {
task.run();
}
}
Восстановление вложенных изображений
Отлично, все работает! Наконец-то можно исправить ошибки - картинки в письме не видны за пределами нашей интрасети.Ведь в наших шаблонах они указаны через public class MailSender { static void sendMailAndRecordHistory(String from, String to, String cc, String key, String params, String attachmentUrls, long docId) throws StateException { .
String embedImgBody = MailUtil.embedImg(body, email); public class MailUtil { static final Pattern HTML_URL = Pattern.compile("<img src=(?:\"|')(.
+)(?:\"|')", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); public static String embedImg(String body, final HtmlEmail email) throws EmailException { return StringUtil.resolveReplacement(body, HTML_URL, new Presentable<Matcher>() { @Override public String toString(Matcher matcher) { String url = matcher.group(1); cid = email.embed(url, UUID.nameUUIDFromBytes(url.getBytes()).
toString()); } return "<img src=\"cid:" + cid + "\""; .
Отправка встроенных (data:image/png;base64,encoded_img) больших изображений.
Новая бизнес-идея — по ошибке отправить скриншот из браузера клиента на почту поддержки.
Решение пользовательского интерфейса найдено - Ход наш.
Для сервиса шаблонов пишем шаблон error_mail.jsp <%@page pageEncoding="UTF-8" %>
<html>
<head>
<meta http-equiv="Content-Type" content="текст/html; кодировка = UTF-8">
<title>Error Report</title>
</head>
<body>
<h2>Error Report from '${user}'</h2>
<b>Message:</b>
<pre>
${message}
</pre>
<b>Screenshot:</b><br>
<img src="${screenshot}">
</body>
</html>
Параметры шаблона — сообщение об исключении и base64_encoded_screenshot — отправляются в TemplateService из нашего сервиса.
У нас проблемы: наша самописная оболочка MyHttpConnection не может отправить base64_encoded_screenshot через GET. Мне приходится делать POST и снова URLEncoder.encode из-за проблем с «+».
Кроме того, картинка не видна во встроенном письме, пришедшем :( Ну и еще ее придется сделать вложением: public class MailUtil {
static Pattern DATA_PROTOCOL = Pattern.compile("^data:(.
+);(.
+),"); public static String embedImg(String body, final HtmlEmail email) throws EmailException { return StringUtil.resolveReplacement(body, HTML_URL, new Presentable<Matcher>() { @Override public String toString(Matcher matcher) { String url = matcher.group(1); String cid; try { Matcher m = DATA_PROTOCOL.matcher(url); if (m.find()) { final String cType = m.group(1); final String encoding = m.group(2); final String content = url.substring(m.toMatchResult().
end()); cid = email.embed(new javax.activation.DataSource() { @Override public InputStream getInputStream() throws IOException { try { return javax.mail.internet.MimeUtility.decode(new ByteArrayInputStream(IOUtil.getBytes(content)), encoding); } catch (MessagingException e) { throw LOGGER.getIllegalStateException("Image encoding failed", e); } } // empty realization for other javax.activation.DataSource methods .
}, UUID.randomUUID().
toString()); } else { cid = email.embed(url, UUID.nameUUIDFromBytes(url.getBytes()).
toString()); } return "<img src=\"cid:" + cid + "\""; .
Последний пункт: безопасность
Однако любой пользователь отправляет запрос на получение из браузера и получает электронное письмо с совершенно секретным документом.Не хорошо.
Необходимо проверить доступ пользователя к документу с переданным docId и вообще проверить: пришел ли запрос через get, авторизован ли пользователь в нашей системе.
В связи с тем, что страница входа уже была создана и вокруг нее много чего происходило, а точка входа в систему у нас была только одна, я сделал проверку через REST и куки на уровне домена с доверием к запросам сервера между сами модули, но это уже - отдельная статья.
Краткое описание простой задачи отправки почты:
В результате получилось 2 maven-модуля с классами (не считая инфраструктуры типа конфигурации, вложений, шаблонов, общих частей и JUnit-тестов).
- почтовый клиент
- MailService: интерфейс (sendMail, sendTemplateMail, sendDocMail)
- MailWSClient: оболочка для клиента, которая предоставляет endPoint из конфигурации.
- mailService.wsdl
- почтовая служба
- MailSender: фактическая отправка
- MailServiceImpl: реализация веб-сервиса, делегирование в MailSender.
- MailServlet: сервлет для обработки HTTP GET.
- MailBean: компонент для чтения строки из базы данных через commons-dbutils.
- MailConfig: конфигурация
- MailScanner: сканирование таблицы по статусу отправки
- MailWebScanner: реализация прослушивателя нашего сервиса, который запускает 2 сканера MailScanner.
- MailUtil: методы восстановления
- EmailExceptionHandler: обработка исключений, не перехватывает AddressException
- sun-jaxws.xml, web.xml
Спасибо за внимание.
Ссылки:
- Руководство пользователя электронной почты Apache
- Примеры dbutils Apache
- Создание скриншота
- Река ERP
-
Кибертерроризм: Ddos-Атаки
19 Oct, 24 -
Улучшите Свой Стиль Заработка В Adsense
19 Oct, 24 -
Нейронные Сети Для Самых Маленьких
19 Oct, 24 -
Это Мой Путь В Китай (Часть 2)
19 Oct, 24 -
Программная Изоляция С Использованием Openvz
19 Oct, 24 -
Объемные Планеты В 2D Через Шейдер
19 Oct, 24