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

В то время клиент пользовался в документах шаблонами 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.

Вот пример правильного письма с точкой:


А вот пример письма без точки, которое получили некоторые неудачливые заказчики:


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

Код сразу пропатчили, потому что команда точно знала причину проблемы. Коллеги ещё раз поблагодарили нас, и мы вернулись к работе.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (31)


  1. nav68
    27.05.2024 14:27
    +8

    Похоже на фантастику.


    1. sigprof
      27.05.2024 14:27
      +15

      Недавно наблюдали похожую ситуацию, но в противоположном направлении — в некоторых случаях точка дублировалась, ломая ссылки в письме. Оказалось, что SwiftMailer (ну да, unsupported и всё такое) при использовании Swift_SendmailTransport смотрит в опции команды sendmail, и если не обнаруживает там опцию -i, дублирует точку в начале строки, как для протокола SMTP, однако реализация /usr/sbin/sendmail от Postfix не производит удаление дублированных таким образом точек во входных данных. Добавление опции -i исправило ситуацию.

      Кроме того, для проявления проблемы также было необходимо, чтобы исходящие письма кодировались в формате Quoted-Printable (при использовании base64 точки в теле письма были бы просто не видны на уровне SMTP), но именно формат Quoted-Printable используется по умолчанию.


  1. Tzimie
    27.05.2024 14:27

    Октетов? Месье француз?


    1. sigprof
      27.05.2024 14:27
      +31

      Старые RFC писались в те времена, когда ещё существовали машины, где байт состоял не из 8 битов (ну или по крайней мере об этом ещё кто-то помнил), поэтому для обозначения 8 бит данных использовался термин «октет».

      Хотя в данном случае слово «octet» в этой части текста появилось только в RFC 5321, а в более старых RFC 2821 и RFC 821 было написано просто «character».


    1. aforism
      27.05.2024 14:27
      +4

      Слово октет довольно часто используется в ит. В частности в сетях.


      1. 0Bannon
        27.05.2024 14:27
        +1

        Например, в http mime-типах есть application/octet-stream.


  1. Wesha
    27.05.2024 14:27
    +6

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


    1. strvv
      27.05.2024 14:27
      +1

      о том что содержимое письма может быть рекурсивно распознано = об этом уже большинство и не знает. о том что адрес может быть типа ааа@bbb@ccc.net или с другой, замудреной системой адресации.

      сам в свое время, в 1999 году, вместо генерации конфига сендмылу, т.к. все эти постфикс, эксим и прочие сервера были не придуманы, правил конфиг вручную.

      как я тогда матерился на создателей...


      1. Wesha
        27.05.2024 14:27
        +3

        адрес может быть типа

        О, адрес!..


  1. ef_end_y
    27.05.2024 14:27

    2й скриншот правильно выдуман? После family должен же быть один перевод строки, а не 2.


    1. ef_end_y
      27.05.2024 14:27
      +1

      Я думаю первая история правда - ибо в этом случае точка будет одна в строке - это и есть признак конца письма. А вторая котолампа, но красивая


  1. voldemar_d
    27.05.2024 14:27
    +1

    Была же какая-то история, когда письма не доходили до городов, находящихся на каком-то расстоянии от отправителя? Вроде, в США было дело.


    1. Wesha
      27.05.2024 14:27
      +4

      Была же какая-то история

      А вот она! — вскричала тётенька.


      1. voldemar_d
        27.05.2024 14:27

        Спасибо, и отдельный респект за тетеньку :)


  1. ErshoffPeter
    27.05.2024 14:27

    Поучительно!


  1. akakoychenko
    27.05.2024 14:27
    +6

    We are pleased to let you know that your new monthly membership fee is now $27.00

    Какой же ублюдочный пассивно-агрессивный оборот с радостной вестью, что теперь надо больше платить за тот же сервис...
    Лично у меня подобный стиль коммуникации вызывает стойкое желание отписаться и перейти к конкурентам, либо, хотя бы сходить в стор и влепить кол, если по объективным причинам сервисом надо продолжать пользоваться


    1. max_mustermann
      27.05.2024 14:27
      +1

      Почему именно больше? Может это наоборот, скидочная цена.


      1. akakoychenko
        27.05.2024 14:27
        +2

        В теме письма fee increase написано


    1. akakoychenko
      27.05.2024 14:27
      +1

      Посыпаю голову пеплом
      Только сейчас, загуглив ключевые фразы со скринов, заметил, что все скрины изначально на 100% выдуманы, и написаны с сарказмом. Таки да, автор специально написал столь подрывающий пукан текст, чтобы добавить юмора...


  1. select26
    27.05.2024 14:27

    RUVDS, если вы вкладываете деньги в рекламу своей безграмотности, то не позорьте хотя бы Героя России А. Лазуткина!
    Энерготребление измеряется не в кВт! В кВт измеряется мощность.


    1. Wesha
      27.05.2024 14:27
      +6

      Энерготребление измеряется не в кВт! В кВт измеряется мощность.

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

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


      1. select26
        27.05.2024 14:27

        Да вы что?
        Откройте квитанцию на оплату электроэнергии и посмотрите на прибор учета потребленной энергии, на основании показаний которого вы оплачиваете ЭЭ. Там везде возле цифирок указана единица их измерения.
        Или откройте любой калькулятор (первый в поисковике: https://kalk.pro/electricity/rashod-elektroenergii/) - вас просят выбрать мощность или потребление. Посмотрите как изменяются единицы измерения.
        А еще лучше - откройте учебник физики, чтобы не рождать такие перлы:
        >энергопотребление — это энергия, потреблённая в единицу времени, так что часы сокращаются, и всё правильно.

        >Я великолепно понимаю Ваш крестовый поход за всеобщую грамотность,

        "как так вышло, что необразованные люди смогли сместить фокус со своей некомпетентности в любом вопросе на универсальный ответ: "Хватит душнить"?" (C)


        1. mayorovp
          27.05.2024 14:27
          +2

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

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

          Откройте квитанцию на оплату электроэнергии и посмотрите на прибор учета потребленной энергии, на основании показаний которого вы оплачиваете ЭЭ. Там везде возле цифирок указана единица их измерения.

          Потреблённая энергия и энергопотребление - не синонимы.


          1. select26
            27.05.2024 14:27

            Как, блин, сокращаются??
            Километров в час - это количество километров за единицу времени. Измеряется в км/ч. Что тут сокращается?
            Это уже не физика даже, а математика начальных классов.

            Вы же не говорите "я ехал со скоростью 50 км"? Или говорите?

            Не хочу спорить. Приведите, пожалуйста, авторитетный источник такой глубокой мысли:
            "нет ничего фундаментально неправильного в измерении энергопотребления просто в киловаттах".
            Я считаю что это именно фундаментально неправильно и на экзамене в школе вы получите неуд. за такой ответ, т.к. в ваттах измеряется мощность, а не энергия.
            Вот источник, подтверждающий мою версию:
            "Единственно правильное написание — «кВт⋅ч» (мощность, умноженная на время) или «киловатт-час».

            Употребление в речевом обиходе в контексте оценки количества произведённой или использованной электроэнергии термина «киловатт» вместо «киловатт-час» формально неправильно, так как мощность не тождественна энергии. Если провести аналогию с механическими величинами — различие между единицами измерения «кВт» и «кВт⋅ч» такое же, как между скоростью и расстоянием.

            https://ru.wikipedia.org/wiki/Киловатт-час

            p.s. Что больше всего поражает - так это количество плюсов в пользу дичайшего бреда. Неужели в РФ так все весело с элементарной бытовой грамотностью? неужели никто не смотрит за что он платит каждый месяц?


            1. mayorovp
              27.05.2024 14:27

              Причём тут вообще километры-то?

              Вы видите на скриншоте единицу измерения? Тим киловатты, умноженные на час и делённые на год. В году примерно 8765,82 часов, поэтому

              \frac{кВт \cdot ч}{год} \approx \frac{1}{8765,82} кВт \approx 0,114 Вт

              Это действительно математика начальных классов.

              Вот источник, подтверждающий мою версию:
              "Единственно правильное написание — «кВт⋅ч» (мощность, умноженная на время) или «киловатт-час».

              Вообще-то это утверждение тут никто и не оспаривает, зачем вы его подтверждаете источниками?


              1. select26
                27.05.2024 14:27

                >Вы видите на скриншоте единицу измерения? Тим киловатты, умноженные на час и делённые на год.

                Вы где это увидели?
                Я вообще в шоке. Не хочу спорить. Считайте как хотите.
                "Число Пи в военное время равно четырем".
                Удачи вам.
                Надо же: "киловатты, умноженные на час и делённые на год."!


                1. mayorovp
                  27.05.2024 14:27

                  Я это увидел по ссылке на калькулятор, которую вы сами и привели. Несколькими комментариями выше скриншот оттуда. Обратите внимание на третье поле ввода (первое справа) и читайте что там написано.

                  Если там написано что-то кроме "кВт * час / год", то скажите что именно.


                1. Wesha
                  27.05.2024 14:27
                  +1

                  Вы где это увидели?

                  Вот здесь
                  символы "/год" видим, нет?
                  символы "/год" видим, нет?

                  Искренне Ваш, Капитан Очевидность.


        1. Wesha
          27.05.2024 14:27

          Откройте квитанцию на оплату электроэнергии

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

          P.S. А знаете, что в этом самое страшное?

          Самое страшное — то, что через каких-то несколько десятков лет эти шлемазлы, ниасилившие физику в объёме курса средней школы, станут президентами ядерных держав. А мы, кто пережил всевозможные Хиросимы с Чернобылями (и ликвидацию их последствий) — повымрем нафиг чисто в силу возраста.

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


    1. FoxWMulder
      27.05.2024 14:27

      честно в энерго я не разбираюсь. но с логикой согласен. если я не упустил чтото важное. нельзя сказать: я потребляю 2 литра воды. потребление не может быть без временного измерения. я потребляю 2 литра воды в день - вот понятно. так же и станция, энергопотрление 14 квт. за какое время?? в секунду? за 100 лет? как без единицы времент понять, много это или мало. так что действительно глупость какаято.

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


      1. mayorovp
        27.05.2024 14:27
        +1

        нельзя сказать: я потребляю 2 литра воды. потребление не может быть без временного измерения

        В киловатте уже есть "временное измерение", потому что ватт - это джоуль в секунду.

        кВт = кДж / с

        энергопотрление 14 квт. за какое время?? в секунду? за 100 лет?

        Это постоянно.

        14 кВт = 14 кВт·ч / ч = 336 кВт·ч / сутки = 123 МВт·ч / год

        Так понятнее?