Repository: prometheus/alertmanager
File: notify/email/email.go (Go)

Code before:
	if _, ok := n.conf.Headers["Message-Id"]; !ok {
		fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), n.hostname))
	}

	if n.conf.Threading.Enabled && len(as) > 0 {
		key, err := notify.ExtractGroupKey(ctx)
		if err != nil {
			return false, err
		}
		// Add threading headers. All notifications for the same alert group
		// (identified by key hash) are threaded together.
		threadBy := ""
		if n.conf.Threading.ThreadByDate != "none" {
			// ThreadByDate is 'daily':
			// Use current date so all mails for this alert today thread together.
			threadBy = time.Now().Format("2006-01-02")
		}
		keyHash := key.Hash()
		if len(keyHash) > 16 {
			keyHash = keyHash[:16]
		}
		// The thread root ID is a Message-ID that doesn't correspond to
		// any actual email. Email clients following the (commonly used) JWZ
		// algorithm will create a dummy container to group these messages.
		threadRootID := fmt.Sprintf("<alert-%s-%s@%s>", keyHash, threadBy, n.hostname)
		fmt.Fprintf(buffer, "References: %s\r\n", threadRootID)
		fmt.Fprintf(buffer, "In-Reply-To: %s\r\n", threadRootID)
	}

	multipartBuffer := &bytes.Buffer{}
	multipartWriter := multipart.NewWriter(multipartBuffer)

	fmt.Fprintf(buffer, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
	fmt.Fprintf(buffer, "Content-Type: multipart/alternative;  boundary=%s\r\n", multipartWriter.Boundary())
	fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n")

	// TODO: Add some useful headers here, such as URL of the alertmanager
	// and active/resolved.
	_, err = message.Write(buffer.Bytes())
	if err != nil {
		return false, fmt.Errorf("write headers: %w", err)
	}

	if len(n.conf.Text) > 0 {
		// Text template
		w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
			"Content-Transfer-Encoding": {"quoted-printable"},
			"Content-Type":              {"text/plain; charset=UTF-8"},
		})
		if err != nil {
			return false, fmt.Errorf("create part for text template: %w", err)

Review comment (bug):
Ah, sorry I didn't notice this before, but the docs for the email integration currently say:
```
# Further headers email header key/value pairs. Overrides any headers
# previously set by the notification implementation.
[ headers: { [<string>](https://prometheus.io/docs/alerting/latest/configuration/#string): [<tmpl_string>](https://prometheus.io/docs/alerting/latest/configuration/#tmpl_string), ... } ]
```

Specifically, we need to do something about:
> Overrides any headers previously set by the notification implementation.

I'm not a fan of how this is implemented elsewhere - It looks like the logic that sets the `Message-Id` header does this via checking if `n.conf.Headers` contains `Message-Id` before setting the value, but there's no check happening when we write user headers... This makes it easy to accidentally introduce bugs.

I also don't think it's appropriate to silently let a user configured value override this behavior - it is a configuration error to set both `threading.enabled` and a custom `References` or `In-Reply-To` header.

In my opinion, the right thing to do is to add a check to the email config's `UnmarshalYAML` implementation which raises an error if threading is enabled and either of these headers is set: https://github.com/prometheus/alertmanager/blob/e542c68a6bc1a2f3ad187cb6b4db8dd925816520/config/notifiers.go#L331-L339

What to do you think?

Code after:
	if _, ok := n.conf.Headers["Message-Id"]; !ok {
		fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), n.hostname))
	}

	if n.conf.Threading.Enabled {
		key, err := notify.ExtractGroupKey(ctx)
		if err != nil {
			return false, err
		}
		// Add threading headers. All notifications for the same alert group
		// (identified by key hash) are threaded together.
		threadBy := ""
		if n.conf.Threading.ThreadByDate != "none" {
			// ThreadByDate is 'daily':
			// Use current date so all mails for this alert today thread together.
			threadBy = time.Now().Format("2006-01-02")
		}
		keyHash := key.Hash()
		if len(keyHash) > 16 {
			keyHash = keyHash[:16]
		}
		// The thread root ID is a Message-ID that doesn't correspond to
		// any actual email. Email clients following the (commonly used) JWZ
		// algorithm will create a dummy container to group these messages.
		threadRootID := fmt.Sprintf("<alert-%s-%s@alertmanager>", keyHash, threadBy)
		fmt.Fprintf(buffer, "References: %s\r\n", threadRootID)
		fmt.Fprintf(buffer, "In-Reply-To: %s\r\n", threadRootID)
	}

	multipartBuffer := &bytes.Buffer{}
	multipartWriter := multipart.NewWriter(multipartBuffer)

	fmt.Fprintf(buffer, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
	fmt.Fprintf(buffer, "Content-Type: multipart/alternative;  boundary=%s\r\n", multipartWriter.Boundary())
	fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n")

	// TODO: Add some useful headers here, such as URL of the alertmanager
	// and active/resolved.
	_, err = message.Write(buffer.Bytes())
	if err != nil {
		return false, fmt.Errorf("write headers: %w", err)
	}

	if len(n.conf.Text) > 0 {
		// Text template
		w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
			"Content-Transfer-Encoding": {"quoted-printable"},
			"Content-Type":              {"text/plain; charset=UTF-8"},
		})
		if err != nil {
			return false, fmt.Errorf("create part for text template: %w", err)