Щоденник одного бага: як я лагодив картинки в електронній пошті

Є внутрішня система, яка крутиться на Weblogic, є готовий шаблон пошти, є програміст і є баг. Ось ви знали, що поштові клієнти з великою ймовірністю не зможуть показати картинку, яка вставлена в розмітку листи, джерело якої починається на
data:image/gif;base64
?

Я, наприклад не знав. Та що вже там, я до цього навіть не знав що картинки можна вставляти в HTML розмітку без, власне, самої картинки. Так вийшло, що одну і ту ж .jsp використовують і для складання сторінки для друку, і, в окремих випадках, для електронного листа. У підсумку в браузері лист відкривається нормально, а поштові клієнти показують биту картинку.

Це буде розповідь про процес знаходження одного рішення. Тепер про все по порядку.

Посеред дня приходить скарга від клієнта, що в листі, який посилає система не показуються картинки. Керівник проекту зробив завдання, дав мені, втім, все як зазвичай. Поліз перевіряти через інтерфейс і справді, мій Outlook видав заготовку з червоними хрестами в квадратах замість картинок. У коді помітив, що той же .jsp файл-шаблон використовується щоб становити дані документа для друку. В той момент я подумав, що як добре, що не треба буде проходити всю бізнес логіку аби отримати листа, а зможу просто дивитись на заготовку для друку. Відкривши заготовочку все виявилося красиво і як треба.

Задачка стала трохи менш приємною, але хто боїться викликів? Спочатку пошерстил інтернет у пошуках способів, як змусити Outlook показати мені розмітку листи, що вдалося досить швидко. В розмітці знайшов, що на відміну від веб інтерфейсу в base64 картинці замість "+" "& #43;". Перша думка була, що метод, який посилає HTML заготовку на SMTP сервер фільтрує якісь спецсимволи. Це було не так, до самого "email.send();" рядок містила всі потрібні символи.

Пора звернутися до компанії добра за напрямком до причин такої поведінки. Через деякий час знайшов ресурс, в якому автор перевіряв такі картинки на сумісність з поштовими клієнтами. У наведеній їм табличці без огріхів показував картинки тільки яблучний клієнт. У коді його листи я побачив заповітний "+" і зрозумів, що це мій випадок. Розповів про відкриття аналітику. Він сказав, що все одно треба лагодити, і заодно поскаржився, що раніше ж було все добре.

А раніше система крутилася на OC4J і після переїзду на Weblogic відносні шляхи теж кудись з'їхали. Через шляхів кидала помилку вже згадана заготівля для друку. Колега заготовочку для друку поправив, але листи або ніхто не помітив, або просто не тестили, адже друк працює. Знайшов метод, де формувалася рядок .jsp, і трохи глибше, знайшов щось цікаве.

Метод-відправник листа
protected boolean sendHtmlEmail(String fromAddress, String fromName, String toAddress, String toName, String subject, String textContent, String htmlContent, String serverName, String serverPort, String serverPath, String docRoot) {

boolean result = true;
HtmlEmail email = new HtmlEmail();

try {
// Email Setup parameters
email.setHostName(SMTP_HOST);
email.setSmtpPort(SMTP_PORT);
email.setCharset("UTF-8");
email.addTo(toAddress, toName);
email.setFrom(fromAddress, fromName);
email.setSubject(subject);
// Add text body
if (!"".equals(textContent)) {
email.setTextMsg(textContent);
}
// Add HTML body with images inline
if (!"".equals(htmlContent)) {
// System.out.println("htmlContent src: " + htmlContent);

Matcher matcher = ManagerBase.imageSrcPattern.matcher(htmlContent);
//URL imageURL = null;
String imageCID = null;
while (matcher.find()) {
//System.out.println("sendHtmlEmail:");
//System.out.println("imageURL done: " + imageURL.toString());

//ClassLoader classLoader = getClass().getClassLoader();
//File file = new File(classLoader.getResource("../../" + matcher.group(2)).getFile());

// Image File = new File(docRoot + matcher.group(2));
// imageCID = email.embed(image/*, matcher.group( 2 )*/);
// System.out.println("imageCID done:" + imageCID);
// htmlContent = htmlContent.replaceFirst(matcher.group(2), "cid:" + imageCID);
}
//System.out.println("htmlContent done: " + htmlContent);
email.setHtmlMsg(htmlContent);

}

// Build && send email
//email.buildMimeMessage();
result = result && filterDebugMail(toAddress, errors);

if (result == true)
email.send();

}
catch (Exception e) {
/ / ......тут обробка помилок, тут нецікаво
/ / ...... хоча і поза спойлера напевно не дуже :)
}

return result;
}


Схоже, що це використовувалося до кодованих картинок? Я подумав, що так. Але CVS мені сказав, що перша версія файлу була вже з таким методом. Але нічого, тут же майже все готово. Почитаю про «cid» і буде все добре. Так я думав…

З початку дослідження проблеми пройшов може годину, хоча швидше всього два. Пора б і що-небудь закодить нарешті. Спочатку треба знайти картинки. У ресурсах тільки шаблони .doc документів, дивно. Знайшов картинки в папці поруч з пачкою .jsp. Гаразд, нехай, але все одно скопирую потрібні мені картинки на ресурси, звідти хоча б стандартним getResource() можна дістати. Так, що треба для cid?

Про! Поряд з HtmlEmail є його приймач ImageHtmlEmail, він явно краще підійде, мені як раз картинки і потрібні. Далі вирішив все таки розділити початковий .jsp і зробив для пошти практично ідентичний минулого (знаю про DRY, не бийте, будь ласка). Але за гайдів поміняв значення src атрибутів на «cid:[назва файлу без розширення]». Не обов'язково повинно було бути назву файлу, але мені так здалося логічніше, тим більше, що патерн для Matcher вже відбирає те, що усередині тега img.
Далі написав щоб система Matcher вибирала правильний файл з ресурсів:

Початок
while (matcher.find()) { 
// знаходимо імг тег, src буде в стилі cid:[ім'я файлу]
String cidInFile = matcher.group(2);
// назва файлу
String imageName = cidInFile.substring(cidInFile.indexOf(":")+1);
// файл в ресурсах
String imageFullResPath = "/images/print/" + imageName + ".gif";
}


Так мені треба запхати картинку в email.embed(), метод вимагає DataSource. Звернення до корпорації добра, 5 нових відкритих вкладок, нове уявлення про новому знайомому. З чотирьох повноцінних класів-реалізацій цікаві виявилися два — ByteArrayDataSource FileDataSource. Але файл передбачає роботу з шляхами, а так як минуле рішення було перероблено з-за шляхів, залишимо це на крайній випадок. ByteArrayDataSource в конструкторі хоче масив байтів і тип даних. Ще одне звернення до добра, ще 7 вкладок. Нехай мене поправлять, якщо я не прав, але тип треба подавати MIME тип. У мене гифки — «image/gif». Власне масив даних отримуємо за допомогою getResourceAsStream() IOUtils apache-commons-io.

Тепер все як то так:

Підлогу шляху
while (matcher.find()) { 
// знаходимо імг тег, src буде в стилі cid:[ім'я файлу]
String cidInFile = matcher.group(2);
// назва файлу
String imageName = cidInFile.substring(cidInFile.indexOf(":")+1);
// файл в ресурсах
String imageFullResPath = "/images/print/" + imageName + ".gif";
// зробимо масив байтів
ClassLoader classLoader = getClass().getClassLoader();
InputStream is = classLoader.getResourceAsStream(imageFullResPath);
byte[] imageData = IOUtils.toByteArray(is);
// embed був нужеу DataSource
DataSource ds = new ByteArrayDataSource(imageData, "image/gif");

String cid = email.embed(ds, imageName);
}


Виглядає набагато краще, тим більше, що коли прогнав через дебагер, помилок не було. Відкоментував email.send(), запускаю NullPointerException, проблема. Начебто все у мене не null, але повторні спроби призводять до тих же результатів. Добре що добро показало, де можна подивитися исходники ImageHtmlEmail, яке кидає NPE. Хмм… там єдине що може кидати NPE — DataSourceResolver, його ж не було в одному з трьох прочитаних навчань про ImageHtmlEmail. Ну ось же — не… є, і в тому є, і в останньому теж.

Спроба додати потрібну зупинилася на виборі імплементації, тому що DataSourceFileResolver , а якогось DataSourceByteResolver — не було.
Тут пропустимо дві години марних спроб все вже написане перевести під FileDataSource. Але в кінці зневірившись, я підгледів, що DataSourceFileResolver не важливо, що у мене за DataSource, якщо src картинки починається з «cid».

Загалом, кінцевий варіант виглядав якось так:

Кінець
protected boolean sendHtmlEmail(String fromAddress, String fromName, String toAddress, String toName, String subject, String textContent, String htmlContent, String serverName, String serverPort, String serverPath, String docRoot) {

boolean result = true;
ImageHtmlEmail email = new ImageHtmlEmail();

try {
// Email Setup parameters
email.setHostName(SMTP_HOST);
email.setSmtpPort(SMTP_PORT);
email.setCharset("UTF-8");
email.addTo(toAddress, toName);
email.setFrom(fromAddress, fromName);
email.setSubject(subject);

// Add text body
if (!"".equals(textContent)) {
email.setTextMsg(textContent);
}

// Add HTML body with images inline
if (!"".equals(htmlContent)) {

URL resurl = getClass().getResource("/");
URI resURI = resurl.toURI();

File resFolder = new File( resURI );
resFolder = resFolder.getParentFile();
DataSourceResolver resolver = new DataSourceFileResolver(resFolder);

email.setDataSourceResolver(resolver);

Matcher matcher = ManagerBase.imageSrcPattern.matcher(htmlContent);

while (matcher.find()) {
// знаходимо імг тег, src буде в стилі cid:[ім'я файлу]
String cidInFile = matcher.group(2);
// назва файлу
String imageName = cidInFile.substring(cidInFile.indexOf(":")+1);
// файл в ресурсах
String imageFullResPath = "/images/print/" + imageName + ".gif";
// зробимо масив байтів
ClassLoader classLoader = getClass().getClassLoader();
InputStream is = classLoader.getResourceAsStream(imageFullResPath);
byte[] imageData = IOUtils.toByteArray(is);
// embed був нужеу DataSource
DataSource ds = new ByteArrayDataSource(imageData, "image/gif");
String cid = email.embed(ds, imageName);

}
// email ready, enjoy your transfer
email.setHtmlMsg(htmlContent);

}

result = result && filterDebugMail(toAddress, errors);

if (result == true)
email.send();

}
catch (Exception e) {
// .........
}

return result;
}


Вважаю за потрібне відзначити, що пропущено і по суті ~6 рядків коду зажадала:

— 6 годин робочого часу
— >30 вкладок з навчаннями, StackOverflow, ітд.
— по одному стриманому міцному слову кожен третій локальний білд
— >30 локальних білдів

Хотів написати собі замітку про знайдене рішення, але раптом комусь ще знадобиться… якщо знайдуть.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.