[PATCH v2 2/2] Add %F expando to $message_id_format to use from address.

"Kevin J. McCarthy" <[email protected]> Tue, 4 Aug 2026 13:18:20 +0800
Newsgroups gmane.mail.mutt.devel
Message-ID <[email protected]>
Filter out characters, even stricter than RFC5322 requires, to allow
the message id to be used in a URL.

If, for whatever reason, the from address is not available, fall back
to "@%f".

Provide an example value in the manual:
  set message_id_format="%z_%F"
noting that %F includes a '@' and so one must not also be inside the
variable.
---
 init.h      |  5 +++++
 messageid.c | 41 ++++++++++++++++++++++++++++++++++++++++-
 protos.h    |  2 +-
 sendlib.c   |  4 ++--
 4 files changed, 48 insertions(+), 4 deletions(-)

diff --git a/init.h b/init.h
index 087ebfa8..d9fc0378 100644
--- a/init.h
+++ b/init.h
@@ -2223,11 +2223,16 @@ struct option_t MuttVars[] = {
   ** The old Message-ID format can be used by setting this to:
   ** ``\fC<%Y%02m%02d%02H%02M%02S.G%c%p@%f>\fP''
   ** .pp
+  ** An alternative to using %f is %F.  This expando includes a '@',
+  ** so when used there must not be a second '@' in the variable:
+  ** ``\fC<%z_%F>\fP''
+  ** .pp
   ** The following \fCprintf(3)\fP-style sequences are understood:
   ** .dl
   ** .dt %c .dd step counter looping from ``A'' to ``Z''
   ** .dt %d .dd current day of the month (GMT)
   ** .dt %f .dd $$hostname
+  ** .dt %F .dd from address of the message, including an '@'
   ** .dt %H .dd current hour using a 24-hour clock (GMT)
   ** .dt %m .dd current month number (GMT)
   ** .dt %M .dd current minute of the hour (GMT)
diff --git a/messageid.c b/messageid.c
index 397ff31c..4bb1d839 100644
--- a/messageid.c
+++ b/messageid.c
@@ -22,6 +22,7 @@
 
 #include "mutt.h"
 #include "mutt_random.h"
+#include "mutt_idna.h"
 
 static char MsgIdPfx = 'A';
 
@@ -30,8 +31,30 @@ typedef struct msg_id_data
   time_t now;
   struct tm tm;
   const char *fqdn;
+  ENVELOPE *env;
 } MSG_ID_DATA;
 
+/* This function is much stricter than RFC5322 specifies, because we also want
+ * the message-id to be passed in a URL as a path segment or parameter without
+ * needing encoding.
+ */
+static void filter_from_addr(char *from)
+{
+  int has_at = 0;
+
+  if (!from)
+    return;
+
+  while (*from)
+  {
+    if (*from == '@' && !has_at)
+      has_at = 1;
+    else if (!strchr(CTYPE_PFCHAR_C, *from))
+      *from = '_';
+    from++;
+  }
+}
+
 static const char *id_format_str(char *dest, size_t destlen, size_t col,
                                  int cols, char op, const char *src,
                                  const char *fmt, const char *ifstring,
@@ -44,6 +67,7 @@ static const char *id_format_str(char *dest, size_t destlen, size_t col,
   unsigned char r_out[4 + 1];
   unsigned char z_raw[12]; /* 32 bit timestamp, plus 64 bit randomness */
   unsigned char z_out[16 + 1];
+  ADDRESS *from;
 
   switch (op)
   {
@@ -107,12 +131,26 @@ static const char *id_format_str(char *dest, size_t destlen, size_t col,
     case 'f':
       mutt_format_s(dest, destlen, fmt, id_data->fqdn);
       break;
+
+    case 'F':
+      from = rfc822_cpy_adr(id_data->env->from, 1);
+      if (!from || !from->mailbox)
+      {
+        snprintf(tmp, sizeof(tmp), "@%s", id_data->fqdn);
+        mutt_format_s(dest, destlen, fmt, tmp);
+        break;
+      }
+      mutt_addrlist_to_intl(from, NULL);
+      filter_from_addr(from->mailbox);
+      mutt_format_s(dest, destlen, fmt, from->mailbox);
+      rfc822_free_address(&from);
+      break;
   }
 
   return (src);
 }
 
-char *mutt_gen_msgid(void)
+char *mutt_gen_msgid(ENVELOPE *env)
 {
   MSG_ID_DATA id_data;
   BUFFER *buf, *tmp;
@@ -123,6 +161,7 @@ char *mutt_gen_msgid(void)
   memcpy(&id_data.tm, gmtime(&id_data.now), sizeof(id_data.tm));
   if (!(id_data.fqdn = mutt_fqdn(0)))
     id_data.fqdn = NONULL(Hostname);
+  id_data.env = env;
 
   fmt = MessageIdFormat;
   if (!fmt)
diff --git a/protos.h b/protos.h
index 7ea95ae5..e03ca0eb 100644
--- a/protos.h
+++ b/protos.h
@@ -156,7 +156,7 @@ void mutt_buffer_expand_multi_path_norel(BUFFER *src, const char *delimiter);
 void mutt_buffer_remove_path_password(BUFFER *dest, const char *src);
 char *mutt_find_hook(int, const char *);
 char *mutt_gecos_name(char *, size_t, struct passwd *);
-char *mutt_gen_msgid(void);
+char *mutt_gen_msgid(ENVELOPE *);
 char *mutt_get_body_charset(char *, size_t, BODY *);
 const char *mutt_get_name(ADDRESS *);
 char *mutt_get_parameter(const char *, PARAMETER *);
diff --git a/sendlib.c b/sendlib.c
index 150bc72d..612b0503 100644
--- a/sendlib.c
+++ b/sendlib.c
@@ -2912,7 +2912,7 @@ void mutt_prepare_envelope(ENVELOPE *env, int final)
     mutt_set_followup_to(env);
 
     if (!env->message_id)
-      env->message_id = mutt_gen_msgid();
+      env->message_id = mutt_gen_msgid(env);
   }
 
   /* Take care of 8-bit => 7-bit conversion. */
@@ -2975,7 +2975,7 @@ static int _mutt_bounce_message(FILE *fp, HEADER *h, ADDRESS *to, const char *re
     fprintf(f, "Resent-Date: %s\n", mutt_b2s(date));
     mutt_buffer_pool_release(&date);
 
-    msgid_str = mutt_gen_msgid();
+    msgid_str = mutt_gen_msgid(h->env);
     fprintf(f, "Resent-Message-ID: %s\n", msgid_str);
     fputs("Resent-To: ", f);
     mutt_write_address_list(to, f, 11, 0);
-- 
2.55.0