Примерно семь лет назад мы работали над проектом по созданию решения, которое должно было позволить клиенту консолидировать все его шаблоны документов в единую систему; это что-то типа системы контроля версий для документов, которые сотрудники клиента рассылали своим заказчикам.
В то время клиент пользовался в документах шаблонами Microsoft Word с замещающим текстом. Каждый раз, когда сотруднику клиента необходимо было отправить документ по электронной почте или распечатать документ для отправки почтовой службой, он заменял весь замещающий текст документа (имя, фамилия и так далее).
В компании на тот момент было множество шаблонов с устаревшими версиями. В некоторых шаблонах использовались устаревшие условия договоров, в других — старый логотип компании или неправильный шрифт и так далее. Системой стало невозможно управлять, и клиент попросил нас найти решение.
В конечном итоге мы придумали решение, позволявшее клиенту создать единый центр отслеживания всех шаблонов, который должен применяться для генерации PDF-документов, SMS-сообщений и тел электронных писем.
В качестве примера настраиваемого шаблона можно привести приветственное письмо новому заказчику. Для каждого типа коммуникации можно было бы настраивать шаблон: для электронной почты, для SMS и для печатной версии, отправляемой обычной почтой. Содержимое каждого шаблона приветственного письма могло разниться в зависимости от механизма доставки (электронная или обычная почта, SMS). В варианте для электронной почты сотрудники могли использовать HTML-таблицы и другие примитивные паттерны стилизации, в бумажной версии они могли добавлять инфографику, в шаблоне SMS — только короткое приветствие.
Спустя несколько месяцев после ввода системы в эксплуатацию мне позвонил один из менеджеров, пользовавшихся нашим ПО.
Он сообщил, что в теле одного из отправляемых заказчику писем отсутствует точка. Самое загадочное было то, что такое происходило только с этим конкретным заказчиком; при отправке того же письма другому заказчику точка не исчезала.
Вот пример приветственного письма с присутствующей точкой:
А вот то же письмо без точки, отправленное другому получателю:
Вот место отсутствующей точки:
В процессе созвона с менеджером я убедился, что в исходном коде шаблона на самом деле есть точка, по словам менеджера, отсутствовавшая в теле письма. Я записал название шаблона (у каждого шаблона было название и версия), чтобы самостоятельно протестировать его после завершения звонка, а затем сообщил менеджеру, что изучу вопрос и что это может занять какое-то время.
После того как менеджер повесил трубку, я скопировал исходный код используемого в продакшене шаблона в мою локальную версию ПО и сгенерировал тело письма для предварительного просмотра. В окне предварительного просмотра тела письма точка была видна. Конкретно этот шаблон мог использоваться и как распечатываемая версия, и как тело электронного письма; в печатной версии, генерируемой из этого шаблона, точка тоже отображалась корректно.
На этом моменте я уже озадачился, ведь я чётко видел точку в исходном коде и в превью в ПО, но менеджер настаивал, что точка отсутствовала в теле письма, полученного конкретным заказчиком.
Я решил активировать код отправки писем. Наше локальное окружение было настроено для отправки писем на определённый порт localhost, после чего имитация SMTP-сервера наподобие SMTP4dev могла получить письмо и отобразить его в установленном локально почтовом клиенте (в моём случае Outlook).
При просмотре локального письма Outlook в теле письма корректно отображалась точка.
Каждый раз при генерации письма, PDF или SMS по шаблону код заменял замещающий код реальным содержимым. Это означало, что каждое отправляемое письмо было уникальным по содержимому тела.
Мне удалось найти отправленное заказчику письмо и посмотреть значения, использованные вместо различных замещающих текстов. Я отправил второе письмо в своё локальное окружение, но теперь использовал те же самые значения.
Затем я открыл локальное письмо в Outlook и действительно убедился, что точка потерялась.
То есть исчезновение точки непосредственно связано с содержимым письма, получаемым конкретным заказчиком.
Я пробовал разное: пытался определить, возможно, символ точки в шаблоне был закодирован, когда мы скопировали его в шаблон, или точка была не точкой, а каким-то другим символом, отображаемым как точка. В общем, хватался за соломинку.
Пробуя всё вышеперечисленное, я отправлял на свой localhost письма после каждого изменения в шаблоне (это было похоже на разработку веб-сайта до того, как появились современные инструменты разработчика — ты жмёшь «Обновить» и пробуешь заново). Я заметил, что при перемещении символа точки в шаблоне из его текущей позиции, допустим, из позиции 5 в строке 4 в позицию 6 в строке 4 точка внезапно начинала отображаться в моём локальном Outlook. Наконец-то у меня появилась зацепка!
Теперь я знал, как воспроизвести проблему, но по-прежнему не понимал её причины. В качестве потенциального решения можно было бы просто переместить точку пробелами и на этом закончить, но мне хотелось добраться до первопричины.
Я приступил к отладке и начал пошагово выполнять код, генерировавший письмо и сохранявший его в локальную базу данных. После вставки в базу данных писем (запланированных на отправку) задача CRON периодически подхватывала эти письма и рассылала их по электронной почте.
Я убедился, что код, сохранявший письмо в базу данных, никак не изменяет шаблон, за исключением подстановки данных заказчика вместо замещающего текста. Затем я сосредоточил своё внимание на коде, вызываемом задачей CRON (планировщиком, рассылавшим электронные письма).
Пошагово прошёлся по коду, вызываемому этой задачей CRON. Этот код мы частично позаимствовали из предыдущего проекта, которым довольно давно занималась одна из наших команд. Одна из частей этого кода реализовывала SMTP-клиент. Я до последнего старался избегать его, но потом у меня больше не оставалось выбора.
Многократно пошагово выполнив этот код и прочитав комментарии в коде, я заметил, что одна из функций в коде проверяла, что каждая строка в теле письма не длиннее определённого количества символов. Если строка превышала этот лимит, то она создавала перенос строки, смещала оставшуюся часть содержимого письма на новую строку и продолжала работу.
Она реализовывала следующую часть спецификации SMTP:
Максимальная общая длина строки текста, включая <CRLF>, составляет 1000 октетов (не считая точки в начале, дублируемую для прозрачности).
Это число можно увеличить при помощи SMTP Service Extensions.
При пошаговом анализе кода я проверил значение переменной, содержащей тело письма, и заметил, что строка, содержавшая пропавшую точку, начинается с символа точки. Это означало, что предыдущая строка превзошла лимит правила длины строки, был создан перенос строки, а символ точки сместился на следующую строку. Вот пример:
Исходное тело письма:
Тело письма после того, как наш SMTP-клиент отформатировал его (не включая другое форматирование):
Поискав информацию о реализации SMTP-клиента, я обнаружил Интернет-страницу, содержащую спецификацию Simple Mail Transfer Protocol (SMTP).
В процессе чтения спецификации я обратил внимание на следующее:
Так как данные почты отправляются по каналу передачи, необходимо указывать конец данных почты, чтобы можно было продолжить диалог команд и ответов (command and reply). SMTP обозначает конец данных почты отправкой строки, содержащей единственную "." (period или full stop).
Там была ссылка на другой раздел спецификации под названием 4.5.2. Прозрачность.
Я просмотрел этот раздел и когда прочитал следующее, чуть не подпрыгнул в кресле:
SMTP-клиент
Перед отправкой строки текста почты SMTP-клиент проверяет первый символ строки. Если это точка, то в начале строки добавляется ещё одна точка.
SMTP-сервер
Когда строка текста почты принимается SMTP-сервером, он проверяет строку. Если строка состоит из единственной точки, то она считается концом индикатора почты. Если первый символ — это точка и в строке есть другие символы, то первый символ удаляется.
В спецификации SMTP-сервера чётко объяснялось, что происходило в нашем случае (исчезновение точки).
Я дополнил код, чтобы он обрабатывал добавление второй точки, если строка начинается с точки и в ней есть другие символы. Теперь при удалении точки SMTP-сервером при получении письма в нём всё равно остаётся точка (это реализовано в SMTP-сервере, получающем письмо и не находящемся под нашим контролем).
Я снова отправил исходное письмо на свой localhost, указав вызывавшие проблемы данные получателя, и на этот раз точка больше не исчезала.
Мы выпустили исправление и сообщили менеджеру, что проблема решена.
Так как код SMTP-клиента был позаимствован из предыдущего проекта, мы решили, что нужно сообщить другим нашим командам о баге на случай, если они тоже захотят его пропатчить. Они поблагодарили нас, и мы решили, что на этом можно закончить историю.
По крайней мере, на этом обычно заканчиваются подобные истории, но не наша. Если бы история закончилась, то, вероятно, я бы не запомнил этот баг.
Прошло несколько месяцев.
Мой менеджер вышел из своего кабинета и сказал что-то вроде «Команда, помните тот баг с пропавшей точкой?».
Наверно, вы видели, как собака или дикое животное двигают ушами, прислушиваясь к далёкому звуку. Я уверен, что именно так двигались мои уши, когда услышали эти слова.
Оказалось, одна из команд не удосужилась пропатчить этот баг в своём коде. К сожалению, поддерживаемая ею система отправила заказчикам кучу очень важных писем, сообщающих о новой сумме ежемесячного платежа. Так получилось, что в небольшой части тел этих писем как раз в нужном месте находилась точка.
Поэтому в части полученных заказчиками писем в новой сумме ежемесячного платежа отсутствовал десятичный разделитель, то есть точка, то есть в письмах говорилось, что новая сумма теперь составляет $2700, а не $27.00.
Вот пример правильного письма с точкой:
А вот пример письма без точки, которое получили некоторые неудачливые заказчики:
К счастью, этот баг сильно зависел от длины каждой строки в теле письма, поэтому лишь немногие заказчики с точно совпадающим количеством символов в имени и фамилии вызывали перенос точки на новую строку и её удаление.
Код сразу пропатчили, потому что команда точно знала причину проблемы. Коллеги ещё раз поблагодарили нас, и мы вернулись к работе.
Автор: ru_vds