Сообщения электронной почты в Discourse неверно сгруппированы в потоки

Заранее приношу извинения за тон некоторых нижеприведённых слов. Я звучу раздражённо, потому что немного раздражён.

Автор: Майкл Браун, через Discourse Meta, 27 июля 2022 г., 14:06:

Извините, я только сейчас догоняю события, вот некоторые мысли, часть из которых уже была рассмотрена…

Сложность здесь в том, что то, что отправляется из Discourse наружу, — это другое сообщение, чем входящее. У него другие метаданные (в данном случае: To/From/Reply-to/Unsubscribe и т. д.) и другое тело (оно настраивается для каждого пользователя (думаю? Разве этого не происходит в режиме рассылки?)).

Что именно представляет собой сообщение? Если следовать RFC 5322 буквально:

Сообщение состоит из заголовков, за которыми необязательно следует тело сообщения.

Поле “Message-ID:” предоставляет уникальный идентификатор сообщения, который относится к конкретной версии конкретного сообщения.
[выделение моё]

Именно фраза “конкретная версия” заставляет меня думать, что было бы неуместно повторно отправлять входящее сообщение с другим Message-ID. Хотя, если сменить точку зрения: рассматривать Discourse не как “программное обеспечение для форумов”, а как “программное обеспечение для рассылки”, то это в какой-то мере имеет смысл, поэтому я понимаю вашу позицию.

К сожалению, это основано на чрезмерно буквальном прочтении, возможно, на чтении контекста, которого нет.

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

Что касается содержимого, как уже упоминалось, почти каждая рассылка добавляет контент в тело письма, обычно это подвал со ссылкой на страницу администрирования списка или ссылкой для отписки. Это также не вызывает изменения message-id.

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

Я вижу, что вы продолжаете цитировать то, что я как раз собирался привести :slight_smile:

RFC 5322 также говорит:

Существует множество случаев, когда сообщения “изменяются”, но эти изменения не представляют собой новую инстанцию этого сообщения, и поэтому сообщение не получает новый идентификатор. Например, когда сообщения вводятся в транспортную систему, к ним часто добавляются дополнительные заголовки, такие как трассировочные поля (описанные в разделе 3.6.7) и поля повторной отправки (описанные в разделе 3.6.6). Добавление таких заголовков не меняет идентичность сообщения, и поэтому оригинальное поле “Message-ID:” сохраняется. Во всех случаях именно смысл, который отправитель сообщения хочет передать (то есть, является ли это тем же самым сообщением или другим сообщением), определяет, изменится ли поле “Message-ID:”, а не какие-либо конкретные синтаксические различия, которые появляются (или не появляются) в сообщении.

Я полагаю, всё сводится к вопросу: меняется ли отправитель сообщения, когда Discourse отправляет его наружу?

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

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

Отправитель — это автор, а не MTA, такой как Discourse.

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

Это всё ещё то же самое сообщение.

Может быть, нам стоит использовать Resent-Message-ID и родственные поля?

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

Оно всегда было там, ещё со времён RFC 822. Но, как вы позже сказали, да, оно было обновлено.

Ой. Я думал, что на тот момент это было только для USENET. Я ошибался.

RFC 5322 также напрямую говорит о том, как Discourse и GitHub его используют:

Поле “In-Reply-To:” может использоваться для идентификации сообщения (или сообщений), на которые отвечает новое сообщение, в то время как поле “References:” может использоваться для идентификации “потока” (thread) обсуждения.

Возможно, немного неправильно, вероятно, из-за отсутствия подходящего заголовка “Thread Identifier”. Но такая интерпретация может не соответствовать намерениям авторов RFC… она не решает проблему сообщений с полем “References”, но без “In-Reply-To”.

Для меня это означает, что оба поля охватывают одну и ту же информацию:

  • References показывает линейную (обычно) ветку обратно к исходному посту (OP)
  • In-Reply-To показывает родительское сообщение и в совокупности с предыдущими сообщениями обратно к OP подразумевает тот же поток

Сложность этого момента в том, что мы отправляем не одно письмо, а N — по одному для каждого получателя — чтобы их индивидуальные метаданные (Unsubscribe и т. д.) были корректными.

Это не сложно. Смысл сообщений одинаков, кастомизации незначительны и семантически не важны. Они не требуют новых или отдельных message-id.

И да, во время тестирования я действительно видел сильные указания на то, что определение спама привязано к Message-ID. Если его позже увидят снова (того же пользователя или другого пользователя), он с гораздо большей вероятностью будет помечен как спам.

Можете показать некоторые из этих случаев? Поскольку message-id позволяют устранять дубликаты, это касается конечного пользователя. И имейте в виду, что многие меры “антиспама” — это заблуждения и мусор. Количество вещей, которые я получал отклонёнными как потенциальный спам по совершенно надуманным причинам… ломать работу электронной почты, чтобы обойти ошибочную фильтрацию спама — плохой выбор.

До сих пор я никогда не копирую людей с адресами GMail, потому что спам-фильтр GMail знает меня и отбрасывает сообщения. Если я отправляю только в список, они получают его. Если я копирую свой адрес GMail, он (а) помечает это как спам и (б) затем также помечает сообщение рассылки как спам (тот же message-id!). Конечный пользователь не видит моё сообщение. Эта логика абсолютно надуманна и неисправима.

[quote=“Cameron Simpson, post:22, topic:233499,
username:cameron-simpson”]
Так что я бы полностью согласился с тем, что вы добавляете ссылку для отписки, специфичную для получателя, и сохраняете оригинальный message-id. Преимущества гораздо-гораздо перевешивают потерю треддинга, если вы дадите каждой копии сообщения индивидуальный message-id.
[/quote]

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

Вздох. Для всех почтовых клиентов. И главная причина, по которой люди в Pythonland говорят, что просто не будут переходить в Discourse, заключается в том, что треддинг по электронной почте сломан. Многие люди не используют форумы, потому что каждый форум требует от них посещения. Электронная почта приходит к ним, они могут использовать свой предпочитаемый читалку и свой предпочитаемый редактор, и треддинг позволяет людям чётко видеть поток обсуждения. Когда это работает.

Текущий формат topic/#{topic_id}/#{post_id}.s#{sender_user_id}r#{receiver_user_id} по крайней мере делает это последовательным для пользователя в его почтовом ящике. Предположение

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

Я бы хотел увидеть доказательства. Списки рассылки делают это правильно по всему миру. Discourse определённо и объективно ломает это. Я пытаюсь это исправить.

Позвольте мне повторить две основные проблемы здесь:

  • Исходный пост (OP) в полях In-Reply-To и References ссылается на фиктивный “до-OP” “topic” message-id, поэтому ни у одного пользователя электронной почты нет треда с исходным сообщением (OP) — всё, включая OP, выглядит как продолжение
  • Письма, полученные через Discourse, и письма, полученные напрямую, например, через CC, имеют разные message-id, хотя семантически это одно и то же сообщение; это ломает треддинг и обнаружение дубликатов

Но я вижу сильный аргумент в пользу того, чтобы заставить Discourse вести себя больше как программное обеспечение для рассылки в режиме рассылки. @martin, я полагаю, что мы не кастомизируем тело сообщения в режиме рассылки? Вы думаете, что имеет смысл采取 более строгий подход к сохранению и повторному использованию Message-ID в режиме рассылки?

Есть люди в Pythonland, которые нашли “режим рассылки” слишком интенсивным, как пожарный шланг. Они хотят получать письма по целевым темам, но не по всем. Обработка message-id должна быть одинаковой для всех сторон электронной почты.

Я человек “режима рассылки” на discuss.python.org. Но я включил его здесь (discourse.org) и *немедленно выключил снова. Мне нужен целевой режим здесь.}

4 лайка

От Майкла Брауна через Discourse Meta, 27 июля 2022 г., 22:37:

Ах! Я думал, что мы уже используем: topic/#{topic_id}/#{post_id}.s#{sender_user_id}r#{receiver_user_id}

{receiver_user_id} создаёт уникальные идентификаторы сообщений для каждого конечного пользователя для одного и того же исходного поста. Это плохо, как только конечные пользователи общаются вне Discourse или получают копии не через Discourse.

Я склоняюсь к тому, чтобы, в интересах баланса между требованиями уникальности и доставляемости электронных писем и требованиями режима рассылки, использовать (2) для режима рассылки, отключённого, и (3) для режима рассылки, включённого.

Как упоминалось в моём недавнем посте, режим рассылки охватывает лишь один из вариантов получения писем в Discourse. Все те же проблемы применимы независимо от того, находится ли получатель письма в режиме рассылки или просто в режиме «письма для некоторых тем/тегов».

Аналогично, в заголовке References я склоняюсь к тому, чтобы его не было для поста №1 в теме

То же самое касается In-Reply-To. Ни один из них не должен присутствовать, потому что для этого они должны ссылаться на фиктивное сообщение, адресованное непосредственно автору темы (OP).

и чтобы он ссылался на тему (то есть topic/#{topic_id}) и на пост, на который идёт ответ, если таковой имеется.

Вы не можете ссылаться на идентификатор сообщения «темы», если не было поста с таким идентификатором сообщения, который был отправлен как письмо. Если вы хотите пойти по этому пути, сделайте исключение для идентификатора сообщения автора темы (OP), сделав его идентификатором сообщения «темы» вместо ...../1.

3 лайка

Это должно быть «до OP». Извините, Кэмерон Симпсон

Как вы и говорите, это именно та проблема, которая нас раздражает:

Я согласен, это должно быть изменено. Идентификатор сообщения ОП должен быть (в отсутствие входящего письма) (упрощённо) topic/1 и не ссылаться на другое сообщение.

Идентификатор сообщения не должен меняться, даже если оно когда-либо было просто постом на Discourse и никогда не было email.

Последующие сообщения могут ссылаться на него.

Почему должен существовать email? Семантически наличие только поста соответствует критериям. Сообщение, на которое он отвечает, существует, просто его нет в почтовом ящике этого человека. Мы пришли к выводу, что именно сообщение является важным, будь то тело поста или тело email. Следовательно, topic/#{topic_id}/1@site является уникальным идентификатором сообщения, ссылающимся на этот пост, независимо от того, находится ли он в email-сообщении или нет.

Это ничем не отличается от получения ответа на email, который ссылается на письмо, которого нет у вас во входящих. Это всё ещё ответ, поэтому поле References легитимно и корректно.

В корне я с вами согласен. Пурист во мне хочет, чтобы всё было правильно. Но практическая необходимость доставлять email в почтовые ящики людей привела к этому. К сожалению, огромное количество людей используют Gmail, никогда не обучают его фильтры, не используют его должным образом и «отписываются», сообщая о спаме[1].

Я согласен, я думаю, мы были немного слишком буквальны, читая:

Идентификатор сообщения относится ровно к одной инстанциации конкретного сообщения

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

  • message_id_from_incoming_email || topic/#{topic_id}/#{post_num}@site (номер поста ОП равен 1)

И каждый раз, когда мы отправляем email, я считаю правильным добавлять References на родительские сообщения вплоть до ОП и устанавливать In-Reply-To на соответствующий стабильный идентификатор сообщения поста (или ОП, если отвечаем на тему), поскольку сообщением является пост. Но эти поля для ОП должны быть пустыми, да.


  1. не то чтобы Gmail сообщал нам об этом, несмотря на то, что мы реализовали цикл обратной связи. ↩︎

5 лайков

Спасибо за ваши ответы @supermathie и @cameron-simpson. Я считаю, что мы достигли консенсуса. Выношу задачи (TODO) в отдельный пост и надеюсь вскоре приступить к их реализации:

  1. Изменить формат генерируемого Message-ID так, чтобы он всегда был <discourse/post/:post_id@:hostname>. Это достаточно уникально, по сути, это возврат к тому, что мы делали раньше. Ссылка на OP теперь будет использовать идентификатор первого поста, а не просто идентификатор темы.
  2. Если у поста есть связанная запись IncomingEmail, мы всегда используем её Message-ID при отправке письма; в противном случае генерируем новый по формату, указанному выше.
  3. Не использовать заголовок References при отправке писем для OP темы, так как на данный момент не на что ссылаться, потому что это первое письмо в потоке.
  4. Обеспечить корректную генерацию заголовков In-Reply-To и References на основе записей PostReply.

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

3 лайка

Просто для уточнения… это будет не hostname сервера, с которого это пришло, а URL сайта? Если это hostname, то мы теряем всю стабильность, когда один и тот же сайт обслуживают 3 разных хоста.

1 лайк

Извините, да, я имею в виду домен сайта, например meta.discourse.org, который получается из Email::Sender.host_for(Discourse.base_url), то есть то, что мы уже используем.

2 лайка

Отличная мысль, я не подумал о перемещениях. Является ли :post_id идентификатором поста (post.id) или его номером (в рамках темы)?

Если это идентификатор поста, мы можем упростить и просто использовать <post/:post_id@:hostname>, так как он никогда не изменится. Тогда нам не нужно хранить Message-ID, если только он не переопределён по умолчанию.

Если нет… то почему бы не использовать здесь идентификатор поста? Нет причин, чтобы эта часть была длинной, главное, чтобы она была уникальной.

2 лайка

Это настоящий идентификатор, а не номер поста.

Это хороший момент, <post/:post_id@:hostname> вероятно будет работать отлично и избавит от необходимости в дополнительном столбце. Возможно, чтобы сделать это более специфичным для Discourse, мы могли бы добавить discourse в начало, например <discourse/post/543563@meta.discourse.org> (помня, что на многих сайтах в имени хоста не будет упоминания о Discourse). Но это уже придирки.

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

Просто мысли вслух, должно быть всё в порядке, я всё равно проверю эти моменты при тестировании изменений :+1:

2 лайка

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

Я колеблюсь. Можно выбрать любой вариант. Но именно в этом суть.

1 лайк

Аргумент в пользу использования только ID сообщения заключается в том, что он более статичен, а это именно то, что нам нужно, поскольку при перемещении сообщения в другую тему ID сообщения останется прежним в заголовке Message-ID, но тема изменится.

Я думаю, что если мы в итоге переместим сообщение и отправим письма из новой темы, то в почтовом клиенте новая ветка всё равно будет создана корректно, поскольку цепочки заголовков References и In-Reply-To будут различаться. В любом случае, я обязательно протестирую этот сценарий и проверю, работает ли он так, как мы ожидаем. Ничего не будет слито в основную ветку, пока различные сценарии не будут работать должным образом.

1 лайк

На основе дальнейших обсуждений @cameron-simpson я обновил список задач (TODO) следующим образом. Публикую их здесь, чтобы вы получили обновление, так как правки в Discourse не придут вам по электронной почте:

  1. Изменить формат генерируемого Message-ID так, чтобы он всегда был <discourse/post/:post_id@:hostname>. Это достаточно уникальный идентификатор; по сути, мы возвращаемся к тому, что делали раньше. Теперь ссылка на исходный пост (OP) будет использовать ID первого поста вместо простого ID темы.
  2. Если у поста есть связанная запись IncomingEmail, мы всегда используем этот Message-ID при отправке письма. В противном случае мы генерируем его, используя формат, описанный выше.
  3. Добавить новое поле outbound_message_id в записи Post, которое будет заполняться либо а) Message-ID входящего письма, если оно создало пост, либо б) исходящим Message-ID, который мы генерируем для постов, созданных через веб-интерфейс Discourse.
  4. Не использовать заголовки References или In-Reply-To при отправке писем для исходного поста (OP) темы, так как пока не на что ссылаться или отвечать, поскольку это первое письмо в теме.
  5. Убедиться, что правильные заголовки In-Reply-To и References генерируются на основе записей PostReply.
1 лайк

Охватывает ли это также цитаты? (Например, если пост цитирует 10 других постов, ссылается ли он на них?)

1 лайк

От Сэма Саффрона через Discourse Meta, 29 июля 2022 г., 02:31:

Это касается ли также цитат (например, пост, в котором процитировано 10 других постов, значит, он ссылается на них?)

In-Reply-To может указывать только на одного предшественника, поэтому выберите одного. References может содержать ссылки на несколько, но RFC явно рекомендует этого не делать, поскольку не все клиентские приложения могут ожидать что-то иное, кроме линейной цепочки от этого поста к исходному сообщению (OP).

Я бы согласился с любым вариантом для References, но склоняюсь к консервативному подходу. Простой алгоритм вычисления:

  • In-Reply-To: используйте message-id первого процитированного сообщения (или любого другого одиночного цитируемого поста, выбранного согласно некоторой политике);
  • References: References того же выбранного поста-предшественника, упомянутого выше, плюс message-id этого же поста.

Это будет стабильно, предсказуемо и корректно.

С уважением,
Кэмерон Симпсон cs@cskk.id.au

2 лайка

Использование поля References таким образом не рекомендуется:

Примечание: Некоторые реализации анализируют поле «References:» для отображения «цепочки обсуждения». Эти реализации предполагают, что каждое новое сообщение является ответом на одно родительское сообщение, и поэтому они могут двигаться назад по полю «References:», чтобы найти родительское сообщение для каждого сообщения, указанного там. Следовательно, попытка сформировать поле «References:» для ответа, имеющего несколько родительских сообщений, не рекомендуется; способ сделать это в данном документе не определён.

2 лайка

От Мартина Бреннана через Discourse Meta, 29 июля 2022 г., 01:57:

На основе этих дополнительных обсуждений, @cameron-simpson, я обновил TODO-список
до следующего вида и публикую его здесь, чтобы вы получили обновление, поскольку
правки в Discourse не придут по электронной почте:

  1. Изменить формат генерируемого Message-ID так, чтобы он всегда был <discourse/post/:post_id@:hostname>. Это достаточно уникально; по сути, мы возвращаемся к тому, что делали раньше. Ссылка на исходный пост (OP) теперь будет использовать ID первого поста вместо простого ID темы.
  2. Если у поста есть связанная запись IncomingEmail, мы всегда используем этот Message-ID при отправке письма; в противном случае генерируем его по указанному выше формату.
  3. Не использовать заголовок References при отправке писем для исходного поста (OP) темы, так как на данный момент не на что ссылаться, поскольку это первое письмо в ветке.

Также я бы исключил заголовок In-Reply-To в письмах для исходного поста (OP).

  1. Убедиться, что заголовки In-Reply-To и References генерируются правильно
    на основе записей PostReply.

Да.

Лично я бы пошёл дальше и добавил отдельный столбец для идентификатора сообщения со стороны электронной почты. Таким образом, после того как вы назначите Message-ID для поста (взяв его из исходного письма, если оно пришло по почте, или сгенерировав, если пост создан через веб-интерфейс), этот идентификатор останется стабильным, независимо от любых изменений в коде сейчас или в будущем. То есть, даже если записи IncomingEmail нет, генерация Message-ID происходит только один раз, а не пересчитывается (что могло бы привести к его изменению).

То есть, сделать его стабильным после создания, сохранив его.

Судя по всему, у вас уже есть связь IncomingEmail. Возможно, у вас есть (или вы можете использовать) связь OutgoingEmail для дополнительного состояния исходящих электронных сообщений, создаваемого в первый раз, когда пост пересылается по электронной почте.

Я знаю, что процесс в основном происходит при создании поста, но могу представить, что в будущем могут появиться такие функции, как:

  • пожалуйста, пересылайте мне письма по всей этой теме, раз я теперь заинтересован;
  • если пост редактируется, рассмотрите возможность отправки обновлённого сообщения с тем же Message-ID.

Вторая идея возникла у меня потому, что у нас есть ещё несколько вопросов для отчёта :slight_smile: Один из них: похоже, Discourse прилагает некоторые усилия, чтобы удалять цитируемую часть ответов, размещённых в начале, чтобы сохранить пост лаконичным, или что-то в этом роде. Несколько недель назад я написал длинный пост в сообществе Python, который был сильно обрезан. Я зашёл и отредактировал его на форуме, восстановив оригинальный текст из своей личной копии. Но один получатель сказал, что у него есть полная версия, и я задумался: отправляет ли Discourse обновления после редактирования как заменяющие сообщения с тем же идентификатором? Это было бы довольно удобно, в зависимости от того, как конечный клиент пользователя обрабатывает такие случаи.

1 лайк

От Мартина Бреннана через Discourse Meta, 29 июля 2022 г., 00:36:

  1. Добавить новое поле outbound_message_id в таблицу Post, чтобы мы могли быть уверены, что поток сохранится даже если сообщение переместится в другую тему или произойдёт что-то подобное; хранить здесь Message-ID для обоих указанных случаев.

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

  1. Не использовать заголовок References при отправке писем для первого сообщения (OP) темы, так как пока не на что ссылаться, поскольку это первое письмо в потоке.
  2. Обеспечить генерацию корректных заголовков In-Reply-To и References на основе записей PostReply и нового столбца outbound_message_id в таблице Post.

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

Ничего нельзя сделать с уже отправленными письмами. Главное — обеспечить правильную организацию потоков в будущем!

Спасибо!
Кэмерон Симпсон cs@cskk.id.au

1 лайк

У нас есть EmailLog, но эти записи очищаются каждые 90 дней, и я не думаю, что это подходящий вариант. Сделаю вот что:

1 лайк

Речь шла о том, чтобы вообще не хранить это… но теперь, если подумать, ID поста никогда не изменится, а вот hostname может. Значит, нам следует сохранять его сразу после сохранения во всех случаях.

Не помешало бы сделать messageid свойством каждого поста, неизменным навсегда…

Разве это не будет другой версией сообщения? Согласно спецификации:

Поле “Message-ID:” предоставляет уникальный идентификатор сообщения, относящийся к конкретной версии определённого сообщения. … Идентификатор сообщения относится ровно к одной версии конкретного сообщения; последующие редакции сообщения получают новые идентификаторы.

Поэтому, вероятно, наш генерируемый message-id должен выглядеть так: <discourse/post/:post_id/rev/:revision_num> (возможно, опуская /rev/:revision_num для первой редакции). Это позволило бы получателям электронной почты получать обновления редактирования, учитывая, что

1 лайк

Да, сделаю это. Что касается остальных обсуждений правок и ревизий, я думаю, это совершенно другой огромный котел с рыбой, в который нам не стоит сейчас ввязываться… давайте сначала исправим наши грехи в организации потоков :slight_smile:

1 лайк