Спрашивается, зачем писать о том, о чём писано-переписано огромное количество раз. Я понимаю и принимаю такую точку зрения и не стал бы поднимать эту тему у себя в блоге, если бы не столкнулся с одной маленькой особенностью, о которой не только сам не подозревал, но и не встречал в документации. Допускаю, при этом, что читал не всё, или, возможно, не то 😕.

Что же это за особенность? Немного предыстории. Есть у меня проект, в котором производятся определенные манипуляции с файлами, ну, если не напускать тумана, осуществляется их загрузка в Oracle UCM. Но это не главное. После того, как файлы загружены (или не загружены) в UCM, производится отправка электронного сообщения о результатах этой операции. Получатели сообщения перечисляются в поле TO через символ-разделитель, а именно "," (запятую).

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

Причина возникшей проблемы вскрылась достаточно быстро - собственно, всё было видно в логах:

...
SEVERE: Invalid Addresses
javax.mail.SendFailedException: Invalid Addresses;
  nested exception is:
	com.sun.mail.smtp.SMTPAddressFailedException: 550 5.1.1 <y.zzzzzz.@ххххххх.com>: Recipient address rejected: User unknown in virtual alias table
...

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

Если честно, я не понимаю, как так получилось, что проблема не проявлялась столько лет. Видимо, по счастливой случайности, увольняющиеся сотрудники не были включены в списки адресатов данных сообщений. Но больше всего меня расстроил тот факт, что при одном неверном (несуществующем) адресе, письмо не отправлялось вообще никому!

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

Я пользовался транспортом, реализованным в библиотеке JavaMail, а в ней, при наличии даже одного "неправильного" адреса, сообщение не отправлялось вообще никому - видимо, программисты то ли не захотели, то ли не успели (за столько-то лет?!) написать какую-то более "продвинутую" реализацию. А может, у них была веская причина поступить именно так, а не иначе. В любом случае, суть проблемы была ясна и надо было что-то с этим делать.

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

  • неправильных адресах (getInvalidAddresses)
  • правильных адресах, по которым доставка была осуществлена (getValidSentAddresses)
  • правильных адресах, по которым доставка не производилась (getValidUnsentAddresses).

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

Блок-схема алгоритма обработки SendFailedException

Немного комментариев к блок-схеме:

  • В случае, если было "выброшено" исключение SendFailedException
    • проверяем, есть хотя бы один "неправильный" адрес
      • если нет, значит исключение вызвано какими-то странными причинами, надо лишь "перевыкинуть" его
      • "неправильные" адреса есть
        • посмотрим, есть ли "правильные" адреса, по которым сообщение не было отправлено
          • если такие адреса есть - отправим по этим адресам наше сообщение, благо, у класса Transport, как-будто специально для таких случаев, есть метод send(Message msg, Address[] addresses), который позволяет переопределить список первоначальных адресатов, указанных в сообщении.
        • выкидываем "специальное" исключение, чтобы вызывающий код понимал, что что-то пошло не так; в качестве первопричины указываем первоначальное исключение SendFailedException (так пытаемся оповестить о том, что есть "неправильные" адреса)

Я не претендую на то, что придумал правильный алгоритм, начиная от понятия "придумал" и заканчивая определением "правильный". Но этот подход позволяет и доставить письма до адресатов (хоть до каких-то), и оповестить заинтересованные службы о том, что надо привести список получателей в соответствие с действительностью (правда, если в вызывающем коде "специальное" исключение будет обработано надлежащим образом, да и службы обратят внимание на сигнал 🙄😏).

Ну и, напоследок, немного кода:

/* Тут предполагается код, подготавливающий сообщение */
...
  try { // пробуем отправить сообщение
      Transport.send(msg); // попытка отправки
  }
  catch(SendFailedException ex) {  
  // Обнаружен интересующий нас Exception
      if ((ex.getInvalidAddresses() != null) && 
          (ex.getInvalidAddresses().length > 0)) {
          // Если были неправильные адреса
          if ((ex.getValidUnsentAddresses() != null) &&
              (ex.getValidUnsentAddresses().length > 0)) 
              // И если есть правильные адреса, по которым можно отправить
              // сообщение, то отправляем
                Transport.send(msg, ex.getValidUnsentAddresses()); 
          // выкинем "специальное" исключение (да, придётся где-то его описать)
          throw new SpecialException("Были обнаружены неправильные адреса", ex); 
      }
      else // неправильных адресов не было - пусть Exception идёт своей дорогой
          throw(ex);
  }
    
...
// может, есть ещё какой-то код.

Конечно, это блок обработки рассчитан только на SendFailedException и не покрывает все возможные исключения, но я и не ставил перед собой неподъёмных задач 😉.

Вот, пожалуй, и всё... пока...