Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ type EmailConfig struct {
Text string `yaml:"text,omitempty" json:"text,omitempty"`
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
TLSConfig *commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
PhantomThreading bool `yaml:"phantom_threading,omitempty" json:"phantom_threading,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,10 @@ tls_config:
# Further headers email header key/value pairs. Overrides any headers
# previously set by the notification implementation.
[ headers: { <string>: <tmpl_string>, ... } ]

# Whether to use Phantom Threading, which results in one thread per day
# instead of one thread per alert.
[ phantom_threading: <boolean> | default = false ]
```

### `<msteams_config>`
Expand Down
28 changes: 28 additions & 0 deletions notify/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), n.hostname))
}

if n.conf.PhantomThreading && len(as) > 0 {
// Add threading headers. All notifications for the same alert
// (identified by fingerprint) on the same day are threaded together.
// The thread root ID is a phantom Message-ID that doesn't correspond to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is where the name "Phantom Threading" comes from? I'm not sure people will understand that based on the name. Maybe "daily threading" would make more sense, although in that case what if we want to do a different threading strategy in the future?

// any actual email. Email clients following the (commonly used) JWZ
// algorithm will create a dummy container to group these messages.
threadRootID := generateThreadRootID(as, n.hostname)
if threadRootID != "" {
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)

Expand Down Expand Up @@ -385,3 +398,18 @@ func (n *Email) getPassword() (string, error) {
}
return string(n.conf.AuthPassword), nil
}

func generateThreadRootID(alerts []*types.Alert, hostname string) string {
if len(alerts) == 0 {
return ""
}

// Use first alert as representative of the alert group.
alert := alerts[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that mean even if we have 30 alerts, whatever the first one is, that's what will be used for fingerprinting? What if that alert resolves, but the other 29 remain?

fingerprint := alert.Fingerprint().String()

// Use current date so all mails for this alert today thread together.
date := time.Now().Format("2006-01-02")

return fmt.Sprintf("<alert-%s-%s@%s>", fingerprint, date, hostname)
}
Loading