Трудозатраты на реализацию «простого» модуля отправки Email в приложении с модульной архитектурой

    На php отправка mail реализуется одной строчкой кода! А на java- нужно 3 недели??!
    (из разговоров с разработчиками и менеджерами)


    Статья не о том, как отправлять почту на java. Моя цель — показать сложности модульной разработки больших приложений (на примере разработки ERP River).

    Итак, задача: реализовать сервис отправки по email (war).

    Этапы разработки:



    Начнем собственно с отправки

    Если не 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">sendmail@mycompany.ru</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>
                  ...            

    Выделение mail-client

    А как теперь соседнему модулю нашей системы быстро дернуть по веб-сервису наш сервис? Проще всего — выделить maven модуль mail-client, сделать от него зависимым наш mail сервис и разрешить любому модулю — нашему клиенту включать в себя (maven dependency) mail-client:
    • Делаем отдельный maven модуль mail-client и кладем в него mailService.wsdl и interface 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);
                      ...                

      Попутно пришлось решить проблему с subject: сервис шаблонов возвращает только тело письма. Шаблон возвращается в формате html, MailUtil выделает из шаблона tiltle и использует его как subject:

              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;
                  }

    Отправляем документ

    Вообще-то у нас документы. А что, если вызывать нас сервис с id документа? Шаблоны для документов в 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. Как side effect имеем историю отправки наших сообщений, можно потом сверху накрутить ui ну и просто 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 запись истории отправки в базу вместе с состоянием (OK или Exception).
      Сканируем таблицу mail_action и на основе состояния state (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 добавим в вызовы MailWSClient флаг async и вызов AsyncExecutor (нашей обертки поверх ScheduledThreadPoolExecutor):
            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();
                    }
                 }

    Чиним вложения картинок

    Олично, все работает! Наконец, можно фиксить баги — картинки в письме не видны снаружи нашего интранета… Ведь они у нас в шаблонах заданы через <img src=«наши внутренние ресурсы», естественно, во всем остальном мире их не увидишь.

    Делаем их встроенными:
            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) большие картинки

    Новая задумка бизнеса — по ошибке из браузера клиента отправлять на support mail скриншот экрана.
    Решение на UI найдено — ход за нами. Для сервиса шаблонов пишем шаблон error_mail.jsp
                <%@page pageEncoding="UTF-8" %>
                <html>
                <head>
                    <meta http-equiv="Content-Type" content="text/html; charset=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>
            

    Параметры шаблона — exception message и base64_encoded_screenshot — отправляются в TemplateService из нашего сервиса. У нас проблемы: наша самописная обертка MyHttpConnection не может через GET отправлять base64_encoded_screenshot. Приходиться делать POST и еще раз делать URLEncoder.encode из за проблем с "+". Кроме того- в пришедшей почте inline картинка не видна :( Что ж, придется ее также делать вложением:
            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 + "\"";
                ...
            

    Финальная точка: безопасность

    Однако, любой пользователь отправляет get запрос из браузера- и получает письмо с совершенно секретным документом. Нехорошо. Необходимо прикрутить проверку доступа у пользователя к документу с переданным docId и вообще проверить: если запрос пришел по по get, залогинен ли пользователь в нашу систему.
    Из-за того, что страница с логином уже была сделана и вокруг нее много что вертелось, а точка входа в систему у нас одна, я сделал проверку через REST и куки уровня домена с доверием к серверным запросам между самими модулями, но это уже — отдельная статья.

    Итоги простой задачи отправки почты:

    В результате получилось 2 maven модуля с классами (не считая инфраструктуры типа конфигурации, вложений, шаблонов, общей части и JUnit тестов)
    • mail-client
      • MailService: интерфейс (sendMail, sendTemplateMail, sendDocMail)
      • MailWSClient: обертка к клиенту, выставляющая endPoint из конфигурации
      • mailService.wsdl

    • mail-service
      • MailSender: собственно отправка
      • MailServiceImpl: имплементация веб-сервиса, делегирование в MailSender
      • MailServlet: сервлет для обработки HTTP GET
      • MailBean: бин для чтения строки из базы через commons-dbutils
      • MailConfig: конфигурация
      • MailScanner: сканирование таблицы по состоянию отправки
      • MailWebScanner: реализация листенера для нашего сервиса, запускающего 2 сканнера MailScanner
      • MailUtil: утильные методы
      • EmailExceptionHandler: обработка exceptions, не доталкивается AddressException
      • sun-jaxws.xml, web.xml


    Терпеливый читатель, дошедший до конца статьи может сам сравнить количество трудозатрат на «задачу отправку почты» и полученную реализацию. Спасибо за внимание.



    Ссылки:

    Метки:
    Поделиться публикацией
    Комментарии 74
    • +46
      Энтерпрайзненько, однако
      • +6
        Да на PHP тоже нужно 3~ недели, если хотите чтоб было по качеству так-же как и у вас. Да и еще и самому протестировать.
        Одна строка, может быть и в java, если вызвать консоль с мейл демоном и передать демону нужные параметры, качество оставит желать лучшего.
        • 0
          Ну… Тут же используется модуль. Если для php тоже взять модуль, вроде SwiftMailer, то три недели не нужны.
        • +2
          спасибо за html2canvas)
          • +20
            Бывает. Накипело. Пора создавать клубы анонимных программистов.
          • +6
            С каждым годом вычислительные мощности компьютеров растут, но для компенсации прироста производительности хитрые разработчики придумали фреймворки.
            • +13
              Вот в этой задаче пригодился бы ассемблер!
            • –2
              Не плохо. Но на Django + Celery + django-celery-email + немного своей логики было бы проще. Да, не было бы внутренних SOAP сервисов, мы просто не используем их. Аутентикация на security декораторах. Шаблониатор и прочее как dependency injection подбираются в рантайме. Да, асинхронно. Да, если задача отвалилась, перезапуститься с дефолтным retry=5, иначе ругаться к админу мылом. Да, с аттачами. Да, в аттачах что угодно. Да быстро. Да scalable. Да, буквально 1-2 дня.

              Не скажу, что подход на Java как то хуже. Просто он и не лучше. Всего то надо попробовать что-то другое, что бы это увидеть.
              • 0
                А через API Outlook еще проще. И шо?)
                • +1
                  При чем тут это? Сложность не в самой отправке, она одной строчкой. Сложность в отказоустойчивости, видимости (каждая задача для Celery видна, можно перезапустить руками или автоматом), удобной интеграции с другими компонентами системы (шаблонизатор, аттачи/документы и т.п.) и удобном использовании компонента, в scalabilty. Так вот это все можно сделать и делают проще. Вместо 3 недель 1-3 дня.
                  • –1
                    По моему мнению, сравнивать пайтон с джавой некорректно — разный класс. Пайтон можно сравнивать с пхп.
                    • +2
                      Обоснуйте? У решения есть некоторые качества, такие как корректное поведение под нагрузкой, scalability, корректная обработка всех edge cases, интеграция в другие компоненты (компонент — не одинокий джекичан). Разве важно на каком языке это все было написано?

                      Другое дело, что архитекура приложения иная. Нет множества SOAP сервисов, что работают вместе и каждый по отдельности это bottle neck, который еще и отдельно нужно скейлить если превышаем нагрузку. Если сервис был написан через пятую точку (более чем реально), то скейлить на несколько машин не получиться, требуется общая VM память/объекты, общие локи и прочие прелести безумного кодинга. В итоге один сервис, джекичан, тянет всех вниз.

                      На питоне все компоненты работают в одном application server. А вот этих серверов может быть сколько угодно. Байткод не занимает особо памяти, поэтому потребление памяти на один app server небольшое. Впереди серверов стоит один/пара front-end серверов, работающих как буфферы (nginx). Все асинхронные задачи выносятся в Celery/подобное, локов просто нет, все сервера stateless (состояние выносится в базу/Redis/RabbitMQ/по задаче). Celery скейлиться на несколько отдельных машин, если задач много. При желании динамически. Рассылка мыла обычно выносится на отдельный MTA, само приложение об этом даже не знает.

                      И все это не что-то типичное для питона. То же самое можно построить на Perl, PHP или Java. Вопрос не языке, а в архитектуре всего приложения. И в необходимом времени. Оно все может занять 3 недели, если писать морду на админку, где видны все задачи в микродеталях, маниакально детальная статистика с десятком графиков. Но я этого из статьи не увидел. Да и ортогонально это все как-то основной задаче.
                      • +3
                        Топ3 решений на пайтоне для энтерпрайза? Их объем рынка?
                        • 0
                          Спасибо, интересная вещь, нужно будет почитать.
                          На java тоже есть асинхронный запуск задач (akka, hazelcast), главное- не стрелять из пушек по воробъям.
                          Soap — он как раз для разнесения серверов и языконезаисимости.
                          Аутентикация на декоратарах не сильно проще ее же в фильтре будет…
                          Питон- хороший язык, но я люблю статическое типизирование. Поэтому счас смотрю в сторону Scala…
                  • 0
                    Вы предлагаете автору переписать проект на Django? :-)
                    • –2
                      Зачем, сравенние — всегда вещь полезная. Я писал на Django совсем немного (и давно), поэтому мое мнение о нем очень субъективное- по поему он мало-объектоно-ориентирован. Фунциональность добавляется через hook-и. Я предпочитаю template method. То же могу сказать о работе в нем с базой.
                  • +10
                    Эта картинка должна быть здесь: problem-factory.jpg.to

                    Простите.
                  • +8
                    На php отправка mail реализуется одной строчкой кода! А на java- нужно 3 недели??!


                    Java, первая ссылка в гугле (отправка gmail):

                    public static void Send(final String username, final String password, String recipientEmail, String ccEmail, String title, String message) throws AddressException, MessagingException {
                            Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
                            final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
                    
                            // Get a Properties object
                            Properties props = System.getProperties();
                            props.setProperty("mail.smtps.host", "smtp.gmail.com");
                            props.setProperty("mail.smtp.socketFactory.class", SSL_FACTORY);
                            props.setProperty("mail.smtp.socketFactory.fallback", "false");
                            props.setProperty("mail.smtp.port", "465");
                            props.setProperty("mail.smtp.socketFactory.port", "465");
                            props.setProperty("mail.smtps.auth", "true");
                    
                            props.put("mail.smtps.quitwait", "false");
                    
                            Session session = Session.getInstance(props, null);
                    
                            // -- Create a new message --
                            final MimeMessage msg = new MimeMessage(session);
                    
                            // -- Set the FROM and TO fields --
                            msg.setFrom(new InternetAddress(username + "@gmail.com"));
                            msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipientEmail, false));
                    
                            if (ccEmail.length() > 0) {
                                msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(ccEmail, false));
                            }
                    
                            msg.setSubject(title);
                            msg.setText(message, "utf-8");
                            msg.setSentDate(new Date());
                    
                            SMTPTransport t = (SMTPTransport)session.getTransport("smtps");
                    
                            t.connect("smtp.gmail.com", username, password);
                            t.sendMessage(msg, msg.getAllRecipients());      
                            t.close();
                        }
                    


                    PHP, первая ссылка в Google отправка через gmail:

                    <?php
                    
                           require_once "Mail.php";
                    
                            $from = "<from.gmail.com>";
                            $to = "<to.yahoo.com>";
                            $subject = "Hi!";
                            $body = "Hi,\n\nHow are you?";
                    
                            $host = "ssl://smtp.gmail.com";
                            $port = "465";
                            $username = "myaccount@gmail.com";  //<> give errors
                            $password = "password";
                    
                            $headers = array ('From' => $from,
                              'To' => $to,
                              'Subject' => $subject);
                            $smtp = Mail::factory('smtp',
                              array ('host' => $host,
                                'port' => $port,
                                'auth' => true,
                                'username' => $username,
                                'password' => $password));
                    
                            $mail = $smtp->send($to, $headers, $body);
                    
                            if (PEAR::isError($mail)) {
                              echo("<p>" . $mail->getMessage() . "</p>");
                             } else {
                              echo("<p>Message successfully sent!</p>");
                             }
                    
                        ?>  <!-- end of php tag-->
                    


                    Вполне сопоставимо по сложности. Преимущество этого Java-кода — он более универсален, можно использовать и для консольных, и для десктопных и для Web. PHP более хардкорный.

                    Хотя ваша идея абсолютно ясна: задачи с одним и тем же названием могут отличаться по сложности в тысячи и десятки тысяч раз. Мой любимый пример — логгер. Самый простой способ логировать в C# — одна строчка: File.Write (ну 2 строчки, если нужен потокобезопасный). Однако почему то возникла необходимость в создании таких продуктов как log4net и NLog — а там работы на несколько человеко-месяцев.
                    • +3
                      PHP скрипт читерский — заюзали pear библиотеку
                      • НЛО прилетело и опубликовало эту надпись здесь
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • –2
                          1. Локальный сервер тоже может лежать.
                          2. SMTP-сервер может просто не справляться с нагрузкой.
                          3. Не все приложения работают на одном компьютере.
                          • +8
                            1. Даже если MTA лежит, исходящая почта будет падать в очередь и отправится как только MTA встанет.
                            2. Как следует из п.1, при любой нагрузке почта теряться не будет, отправится рано или поздно. Ну и предпосылка о том, что удаленный SMTP-сервер уж точно справится с нагрузкой, несколько сомнительна.
                            3. MTA можно поставить на каждый сервер и указать всем, например, единый смартхост, через который они будут пулять исходящую почту. Абсолютно нормальное решение. Уж точно лучше попытки сделать свой отказоусточивый велосипед :)
                            • НЛО прилетело и опубликовало эту надпись здесь
                            • –1
                              Интересное решение.
                              Но — из триггера в базе уже не отправишь. Так получилось универсально.
                              К тому же на доталкивание можно логику повесить.
                              • НЛО прилетело и опубликовало эту надпись здесь
                            • +2
                              Ну Вы смешали задачи совершенно не относящиеся к отправке почты: сервлеты, веб-сервисы, шаблоны (кстати я использую отдельный шаблон для темы и предпочитаю freemarker). Но, в целом, Вы правы, это не простая задача. Например для Grails я разработал плагин Asynchronous Mail, который позволяет отправлять почту в одну строчку :). Но он тянет за собой 3 плагина, которые тянут 2 фрэймворка.
                              • –1
                                «б.., это… здец»
                                • +1
                                  для ведения промышленной разработки на РНР, также используеся модульная архитектура,
                                  а тем более рассылка — дело довольно-таки не тривиальное… И чтоб сделать толковый модуль и внедрить его в существующую систему, даже используя готовые классы PHPMailrr или Pear потребуется ни одна пара-тройка дней.
                                  • +1
                                    Да уж, чего только не придумают, чтобы не набрать

                                    yum install postfix
                                    

                                    ;-)
                                    • 0
                                      И что это дает, кроме инсталляции постфикса?
                                      • +9
                                        Вся статья сведется к вызову mail (sendmail) и скармливанию заполненного шаблона. Ну и обертке jax-ws к этому (или сервлету? что там автору надо-то).

                                        И недоступность отработает, и доставку, и очереди. И scalability будет такое, что самопальным наколеночным поделиям и не снилось. А самое смешное — не понадобится ни конфигурация (зачем? на отсылку-то стандартный смартхост из коробки!), ни эм-бинов с БД, ни прочих ентерпрайзиков.

                                        Заодно и узнаете, что mail client это нечто другое ;-)

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


                                        • 0
                                          1. Нужна отсылка по вставке записи в базу (BPM создает запись, когда пользователю назначается задание). Как вы ее предлагаете решить через postfix?
                                          2. Окружений у нас — 6 штук. Бывает гораздо больше. Услилия на «самопальное наколеночное поделие»- ксати достаточно простое- очень даже сравнимы на инсталирование MTA (переконфигурирование, поддержание на всех окружениях)
                                          2. Конфигурация в многослойном таком приложенни — есть. Такой факт. Я сторонник держать все ее в одном месте, а не распылять по конфигам. Только если без этого не обойтись. Например- по умолчанию отправка от чъего имени, или суппорт майл — настройки почты, но никак MTA не отдать.
                                          PS: а последняя фраза- красивая, не спорю.
                                          • +11
                                            (смотрит) ну давайте мы внезапно еще что-нибудь узнаем про вашу задачу.

                                            Про окружение посмеялся. Да-да, а перенастраивать все остальное как бы не надо. Само заработает.

                                            * * *

                                            У меня для вас есть рассказ — в далеком 1995м году я был такой же молодой и горячий. И тоже рисовал наколеночную отсылку почты, ровно в таком же ентерпрайзненьком стиле. Все было замечательно, пока не поставили этого кадавра клиентам. И тут! Тут началось.

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

                                            Однако, за субботу с воскресеньем было налеплено столько писем, что на диске закончилось место. БД с логами за него подрались. Поинтересуйтесь, что в этом случае делает MySQL — вам понравится. А поскольку кроме почты БД пользовались и другие — оно все тоже встало колом.

                                            И вот в понедельник с утра — пришел дежурный инженегр. Увидев, что все встало, он подцепил сетевой шнурок. Однако это не помогло. Обнаружив что на диске закончилось место, он удалил что-то ненужное.

                                            Это не конец истории — дальше было вот что. Могучий досылатель, радостно заметив что почтовый сервер появился, тут же «дослал». SMTP там стоял какой-то древний демон на новеле, и от такого подарка он тут же сдох. Ну правильно — теперь на почтовом сервере место закончилось.

                                            Новелевское чудо тоже было в ентерпрайзненьком стиле — и пришлось выгребать помои руками. Сначала из очереди, а потом из мейлбоксов — часть спама таки доехала.

                                            Что же было дальше? А вот что, досылатель снова обнаружил что почтовый сервер работает, и снова дослал новую порцию (да, я был креативным, и не пытался все письма залить сразу. Я разбил на порции. Предусмотрительный был, ага. А то бы сдох кадавр, и проблему решили бы быстрее).

                                            Бам!

                                            В понедельник ближе к ночи была найдена причина этих «бам». Разумеется, это был наш кадавр — но попробуй это определить, когда у тебя серверная далеко не из двух компов состоит, а начальство размахивает руками над ухом.

                                            И вот под утро вторника кадавру почистили очередь руками в базе, не разбираясь — а было ли там хоть что-то полезное, база данных отсвопалась после освобождения места на диске, и все это чудо наконец-то заработало в штатном режиме.

                                            А через месяц вечером к большому боссу клиента пришел старинный приятель. И потребовал чаю. И секретарша, не найдя розетки поближе, выдернула раутер и включила чайник. О ее умственных способностях она узнала только утром. Чайник выдернули, вернули ценный аппарат на место… но кадавр-то не обесточен!

                                            БАМ!!!

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

                                            В новой инкарнации, отсылатель почты отработал десять лет без единого сбоя. Менялась инфраструктура, сервера и версии софта, но древний монстр, написанный на java 1.0.2, все так же тихо работал на древнючем ультраспарке под не менее древним SunOS.

                                            К слову, 10 лет спустя я был свидетелем демонтажа этого спарка. Единственный раз в жизни я видел слой пыли в системнике, который слежался до такой степени, что из него сразу можно было делать валенки. Первая мысль электронщика была «а зачем в корпус кусок пальто положили?»

                                            Вы говорите — поцфикс не решает? ;-)
                                            • 0
                                              Интересная история, спасибо. Тоже посмеялся. Ну и есть чему поучиться на чужих ошибках.
                                              ps: поставлю на аварийную отсылку ограничитель. В уме это конечно было, даже озвучил- но отклика вверху не нашло, поэтому оставил. А тут вижу- не надо лениться.
                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                • 0
                                                  Не читал, спасибо. Делал я как то SMS сервис (тоже в SOA). Все гораздо сложнее:)
                                                  Если это конечно действительно ентерпрайз.
                                                  1. Нужно отделить интерфейс от реализации- в разных странах разные провайдепы, да и внутри себя иногда их меняют.
                                                  2. Было требование на ограничение по времени и количеству на одного клиента и на систему в целом- (в день- не больше 200, в минуту- не больше 5 например). Причем задаваемое в конфигурации. Не совсем простая задача, когда начинаешь реализовывать.
                                                  3. Нужны были счетчики по событиям и статистика. Не меньше этого сервиса получилось:)
                                              • +1
                                                Напомнило: В одной из больниц Йоханнесбурга (ЮАР) стали повторяться необъяснимые случаи смерти пациентов после операций — серьезных, но прошедших, казалось бы, вполне успешно. Причем все такие случаи происходили по пятницам.

                                                Расследование показало, что в пятницу в отделение хирургии приходила уборщица. Убирая в реанимационной палате, она ничтоже сумняшеся выдергивала вилку установки «искусственное сердце» из розетки в стене и включала свой пылесос… (http://www.nkj.ru/archive/articles/4083/?sphrase_id=83855)
                                                • +1
                                                  Интересно, сколько человек вам поверило?
                                                • 0
                                                  Это достойно отдельного топика -) На хабре нет раздела «байки»? :)
                                        • 0
                                          Если известно, что приложение будет исполняться в типичном UNIX-окружении, то, несмотря на обратное утверждение автора статьи, настройки SMTP-сервера можно и захардкодить: 127.0.0.1:25 без авторизации, т.к. в рассматриваемом окружении это всегда правильно. Это уже дело локального почтового сервера переслать письмо через правильный почтовый релей с авторизацией под правильной учетной записью.
                                          • 0
                                            Хардкодить в принципе не правильно. Да, можно зарелеить автоматом, но перед отправкой локальный серврер может сбрасывать мыло на диск, чтобы в максимально короткое время принятъ мыло полностью и освободить клиента. А если обзваниваем большую базу клиентов, да с аттачами, это может быть весьма весомо. При криворукости админа система начинает жевать своп, все замедляется тысячи раз, отправка нового мыла заморожена. Если мыло-сервер еще и на одной машине с чем то важным (база, app server, все зависит от фантазии админа), то и эти сервисы замораживаются. Всякие сценарии возможны.
                                            • 0
                                              Хотя, все таки нереально это как то все. Извиняюсь за коммент, не могу удалить.
                                            • 0
                                              Правильнее все-таки дать возможность настроить smtp, но предусмотреть умолчания — localhost:25 и без пароля.
                                            • 0
                                              Если известно, что приложение будет исполняться в типичном UNIX-окружении, то, несмотря на обратное утверждение автора статьи, настройки SMTP-сервера можно и захардкодить: 127.0.0.1:25 без авторизации, т.к. в рассматриваемом окружении это всегда правильно. Это уже дело локального почтового сервера переслать письмо через правильный почтовый релей с авторизацией под правильной учетной записью.

                                              Или тупо вызвать sendmail, который работает (складывает письмо в очередь) даже если собственно smtp-демон в данный момент перезапускается. Лишь бы хватало места и inodes.
                                              • +1
                                                Тут поступил приватный коммент от Elmot- некаширно использовать GET для действий — можно получить спам: forums.asp.net/t/1822324.aspx/1
                                                В оправдание скажу только что это все таки интранет приложение и чз GET из браузера сделать ссылку проще. Но в целом- согласен. Можно подумать на тему- оставить только post.

                                                • +4
                                                  Да уж, столько понаписали, и такой косяк с GET-ом. В этом вся Java — куча настраиваемых и конфигурируемых (недельки за три) компонентов, за которыми не видно базовых вещей.
                                                  GET здесь совершенно, ну совершенно не в кассу.
                                                  Объясню на пальцах: вы, вообще говоря, не контроллируете, как браузер или другой http-клиент будет использовать GET-методы, благо стандарт HTTP, особенно если его «вольно трактовать» и «оптимизировать», это позволяет. Клиент может, если это старый ИЕ или по дороге есть кривой прокси, вообще не вызывать GET — а фигли, закешировано же.
                                                  Ну и главное, конечно, безопасность. Отправляем вашему залогиненному юзеру например html емейл с картинкой <img src='ваш get url'> и всё — почта отправится «сама».
                                                  Для подобных действий — только POST. Для хардкорных html-кодеров есть формы, которые ничуть не сложнее просто ссылок; для js вызов get и post вообще ничем не отличаются, кроме названия метода.
                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                                    • 0
                                                      Действительно- еще не пользовал. Но в данном случае- он неуместен:)
                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                                      • 0
                                                        Всё же у JAX-RS и JAX-WS цели несколько разные. И когда нужна тонна энтерпрайзных стандартов WS-*, то REST нервно курит в сторонке, к сожалению.
                                                        • НЛО прилетело и опубликовало эту надпись здесь
                                                          • 0
                                                            Ну да, хоть и не соответствует идеологии REST, т. к. отнюдь не является идемпотентным действием.
                                                            • НЛО прилетело и опубликовало эту надпись здесь
                                                              • 0
                                                                Технически — да.

                                                                JAX-RS — это Java API for RESTful Web Services, так что идеологически JAX-RS предназначен для RESTful сервисов, что и задает рамки.

                                                                Поэтому REST-подход вполне ожидаем (и вызывает наименьшее удивление) при использовании JAX-RS. Я говорю исключительно про это.

                                                                А то, что в каком-то сервисе GET-запрос будет являться не идемпотентным, может дать дыру в безопасности — другая сторона вопроса.
                                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                                  • 0
                                                                    Можно посмотреть и с этой стороны. Но в случае Java JAX-RS наиболее подходит для RESTful сервисов.

                                                                    Как вспомню restlet'ы, аж передёргивает.
                                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                        • –2
                                                          Аутентификацию прикрутить и всё.
                                                          Да и откуда инфа, что GET этот будет из браузера вызываться а не из какого-то клиента в качестве RESTful сервиса.
                                                          И заменить GET на POST в том коде — дело двух секунд.
                                                          • 0
                                                            Аутентификация не поможет, если юзер уже залогинен на атакуемом сервисе и все нужные куки у него уже стоят. Пример — в одной вкладке браузера открыт атакуемый сервис, в другой — веб-почта с веселыми картинками. CSRF токены тоже могут не справиться, хотя спасут хотя бы от повторяемых множественных атак (скажем, от десятка «картинок» в одном письме).
                                                            Смысл в том, что использовать в данном случае GET вместо POST — это всё равно, что использовать SELECT… PROCEDURE вместо INSERT для создания записей в базе данных или забивать шурупы молотком. Теоретически возможно, но абсолютно безграмотно.
                                                            • 0
                                                              Аутентификация есть. Но от ссылки в мыле залогиненного пользователя- не спасет. Post тоже есть в CommonServlet (см.
                                                              public class MailServlet extends CommonServlet). Запретить GET- ерунда. Только клиена UI еще придется с get на post перевести, ну и JUnit-ы поправить.
                                                            • 0
                                                              И на старуху бывает проруха…
                                                          • 0
                                                            >А если сервер временно недоступен? Нужно сохранять историю в базе и делать доталкиватель…

                                                            Вообще-то с этого надо было начать — при нажатии кнопки «Отправить», создается запись в базе.
                                                            Ну а потом по базе бегает сервис, смотрит, что нужно отправить ну и отправляет.
                                                            • 0
                                                              Записывается в блоке final. С результатами отправки.
                                                            • 0
                                                              А тебе, пожалуйста, конкретное требование заказчика в студию.
                                                              • 0
                                                                *теперь, простите
                                                                • 0
                                                                  Заказчик фичи- бизнес (аналитики).
                                                                  Отправлять почту (желательно по веб-сервису). С доталкивателем.
                                                                  + позже — по ошибке из браузера клиента отправлять на support mail скриншот экрана.

                                                                  Детали реализации, интеграция с имеющимися модулями и с окружением — тут уж я сам… Например, самому сходить за шаблоном: удобнее пользоваться будет, асинхронность, конфигурирование и пр.
                                                              • 0
                                                                Я теперь буду показывать эту статью людям, говорящим «ничего сложного» :)
                                                                • 0
                                                                  А Вы все-таки смухлевали! Вместо apache commons надо было использовать труЪ JavaMail API ;))
                                                                  • 0
                                                                    Это вы к изобретению собственных велосипедов? Я сторонник пользоваться готовым.

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