Skip to content

Commit ab70ee3

Browse files
committed
feat: implement phantom_threading to group email alerts into threads
Some email clients such as Gmail apparently use their own heuristics for threading and already implement this behavior based on the subject. But for users of other email clients that only implement threading based on the relevant headers (e.g. notmuch), those users currently get one email thread for each newly firing alert. With phantom_threading enabled, all alert emails (of the same alert) on the same day are grouped into the same thread. Much nicer :) Signed-off-by: Michael Stapelberg <[email protected]>
1 parent 97bbb5a commit ab70ee3

File tree

3 files changed

+33
-0
lines changed

3 files changed

+33
-0
lines changed

config/notifiers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ type EmailConfig struct {
304304
Text string `yaml:"text,omitempty" json:"text,omitempty"`
305305
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
306306
TLSConfig *commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
307+
PhantomThreading bool `yaml:"phantom_threading,omitempty" json:"phantom_threading,omitempty"`
307308
}
308309

309310
// UnmarshalYAML implements the yaml.Unmarshaler interface.

docs/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,10 @@ tls_config:
986986
# Further headers email header key/value pairs. Overrides any headers
987987
# previously set by the notification implementation.
988988
[ headers: { <string>: <tmpl_string>, ... } ]
989+
990+
# Whether to use Phantom Threading, which results in one thread per day
991+
# instead of one thread per alert.
992+
[ phantom_threading: <boolean> | default = false ]
989993
```
990994

991995
### `<msteams_config>`

notify/email/email.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,19 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
265265
fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), n.hostname))
266266
}
267267

268+
if n.conf.PhantomThreading && len(as) > 0 {
269+
// Add threading headers. All notifications for the same alert
270+
// (identified by fingerprint) on the same day are threaded together.
271+
// The thread root ID is a phantom Message-ID that doesn't correspond to
272+
// any actual email. Email clients following the (commonly used) JWZ
273+
// algorithm will create a dummy container to group these messages.
274+
threadRootID := generateThreadRootID(as, n.hostname)
275+
if threadRootID != "" {
276+
fmt.Fprintf(buffer, "References: %s\r\n", threadRootID)
277+
fmt.Fprintf(buffer, "In-Reply-To: %s\r\n", threadRootID)
278+
}
279+
}
280+
268281
multipartBuffer := &bytes.Buffer{}
269282
multipartWriter := multipart.NewWriter(multipartBuffer)
270283

@@ -385,3 +398,18 @@ func (n *Email) getPassword() (string, error) {
385398
}
386399
return string(n.conf.AuthPassword), nil
387400
}
401+
402+
func generateThreadRootID(alerts []*types.Alert, hostname string) string {
403+
if len(alerts) == 0 {
404+
return ""
405+
}
406+
407+
// Use first alert as representative of the alert group.
408+
alert := alerts[0]
409+
fingerprint := alert.Fingerprint().String()
410+
411+
// Use current date so all mails for this alert today thread together.
412+
date := time.Now().Format("2006-01-02")
413+
414+
return fmt.Sprintf("<alert-%s-%s@%s>", fingerprint, date, hostname)
415+
}

0 commit comments

Comments
 (0)