Skip to content

Commit 4a5ee05

Browse files
authored
feat(smtp): add support for custom client host (#271)
1 parent af75ade commit 4a5ee05

File tree

6 files changed

+179
-69
lines changed

6 files changed

+179
-69
lines changed

internal/failures/failure.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type FailureID int
88
type failure struct {
99
message string
1010
id FailureID
11-
stack string
11+
wrapped error
1212
}
1313

1414
// Failure is an extended error that also includes an ID to be used to identify a specific error
@@ -18,19 +18,27 @@ type Failure interface {
1818
}
1919

2020
func (f *failure) Error() string {
21-
return fmt.Sprintf("%s: %s", f.message, f.stack)
21+
if f.wrapped == nil {
22+
return f.message
23+
}
24+
return fmt.Sprintf("%s: %v", f.message, f.wrapped)
25+
}
26+
27+
func (f *failure) Unwrap() error {
28+
return f.wrapped
2229
}
2330

2431
func (f *failure) ID() FailureID {
2532
return f.id
2633
}
2734

35+
func (f *failure) Is(target error) bool {
36+
targetFailure, targetIsFailure := target.(*failure)
37+
return targetIsFailure && targetFailure.id == f.id
38+
}
39+
2840
// Wrap returns a failure with the given message and id, saving the message of wrappedError for appending to Error()
2941
func Wrap(message string, id FailureID, wrappedError error, v ...interface{}) Failure {
30-
var stack string
31-
if wrappedError != nil {
32-
stack = wrappedError.Error()
33-
}
3442

3543
if len(v) > 0 {
3644
message = fmt.Sprintf(message, v...)
@@ -39,7 +47,7 @@ func Wrap(message string, id FailureID, wrappedError error, v ...interface{}) Fa
3947
return &failure{
4048
message: message,
4149
id: id,
42-
stack: stack,
50+
wrapped: wrappedError,
4351
}
4452
}
4553

internal/testutils/textconfaker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ func (tcf *textConFaker) GetConversation(includeGreeting bool) string {
4141
if len(tcf.responses) > ri && !inSequence {
4242
resp = tcf.responses[ri]
4343
}
44+
45+
if query == "" && resp == "" && i == len(input)-1 {
46+
break
47+
}
48+
4449
conv += fmt.Sprintf(" #%2d >> %50s << %-50s\n", i, query, resp)
4550
for len(resp) > 3 && resp[3] == '-' {
4651
ri++

pkg/services/smtp/smtp.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package smtp
33
import (
44
"crypto/tls"
55
"fmt"
6-
"github.com/containrrr/shoutrrr/pkg/format"
76
"io"
87
"math/rand"
98
"net"
109
"net/smtp"
1110
"net/url"
11+
"os"
1212
"time"
1313

14+
"github.com/containrrr/shoutrrr/pkg/format"
1415
"github.com/containrrr/shoutrrr/pkg/services/standard"
1516
"github.com/containrrr/shoutrrr/pkg/types"
1617
)
@@ -64,16 +65,16 @@ func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) e
6465

6566
// Send a notification message to e-mail recipients
6667
func (service *Service) Send(message string, params *types.Params) error {
67-
client, err := getClientConnection(service.config)
68-
if err != nil {
69-
return fail(FailGetSMTPClient, err)
70-
}
71-
7268
config := service.config.Clone()
7369
if err := service.propKeyResolver.UpdateConfigFromParams(&config, params); err != nil {
7470
return fail(FailApplySendParams, err)
7571
}
7672

73+
client, err := getClientConnection(service.config)
74+
if err != nil {
75+
return fail(FailGetSMTPClient, err)
76+
}
77+
7778
return service.doSend(client, message, &config)
7879
}
7980

@@ -106,6 +107,12 @@ func getClientConnection(config *Config) (*smtp.Client, error) {
106107

107108
func (service *Service) doSend(client *smtp.Client, message string, config *Config) failure {
108109

110+
clientHost := service.resolveClientHost(config)
111+
112+
if err := client.Hello(clientHost); err != nil {
113+
return fail(FailHandshake, err)
114+
}
115+
109116
if config.UseHTML {
110117
service.multipartBoundary = fmt.Sprintf("%x", rand.Int63())
111118
}
@@ -149,6 +156,20 @@ func (service *Service) doSend(client *smtp.Client, message string, config *Conf
149156
return nil
150157
}
151158

159+
func (service *Service) resolveClientHost(config *Config) string {
160+
if config.ClientHost != "auto" {
161+
return config.ClientHost
162+
}
163+
164+
hostname, err := os.Hostname()
165+
if err != nil {
166+
service.Logf("Failed to get hostname, falling back to localhost: %v", err)
167+
return "localhost"
168+
}
169+
170+
return hostname
171+
}
172+
152173
func (service *Service) getAuth(config *Config) (smtp.Auth, failure) {
153174

154175
switch config.Auth {
@@ -183,7 +204,7 @@ func (service *Service) sendToRecipient(client *smtp.Client, toAddress string, c
183204
}
184205

185206
if err := writeHeaders(wc, service.getHeaders(toAddress, config.Subject)); err != nil {
186-
return fail(FailWriteHeaders, err)
207+
return err
187208
}
188209

189210
var ferr failure
@@ -256,7 +277,7 @@ func (service *Service) writeMessagePart(wc io.WriteCloser, message string, temp
256277
return fail(FailMessageTemplate, err)
257278
}
258279
} else {
259-
if _, err := fmt.Fprintf(wc, message); err != nil {
280+
if _, err := fmt.Fprint(wc, message); err != nil {
260281
return fail(FailMessageRaw, err)
261282
}
262283
}
@@ -282,13 +303,16 @@ func writeMultipartHeader(wc io.WriteCloser, boundary string, contentType string
282303
return nil
283304
}
284305

285-
func writeHeaders(wc io.WriteCloser, headers map[string]string) error {
306+
func writeHeaders(wc io.WriteCloser, headers map[string]string) failure {
286307
for key, val := range headers {
287308
if _, err := fmt.Fprintf(wc, "%s: %s\n", key, val); err != nil {
288-
return err
309+
return fail(FailWriteHeaders, err)
289310
}
290311
}
291312

292313
_, err := fmt.Fprintln(wc)
293-
return err
314+
if err != nil {
315+
return fail(FailWriteHeaders, err)
316+
}
317+
return nil
294318
}

pkg/services/smtp/smtp_config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Config struct {
2525
Encryption encMethod `desc:"Encryption method" default:"Auto" key:"encryption"`
2626
UseStartTLS bool `desc:"Whether to use StartTLS encryption" default:"Yes" key:"usestarttls,starttls"`
2727
UseHTML bool `desc:"Whether the message being sent is in HTML" default:"No" key:"usehtml"`
28+
ClientHost string `desc:"The client host name sent to the SMTP server during HELLO phase. If set to \"auto\" it will use the OS hostname" key:"clienthost" default:"localhost"`
2829
}
2930

3031
// GetURL returns a URL representation of it's current field values

pkg/services/smtp/smtp_failures.go

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,35 +43,56 @@ const (
4343
FailCreateSMTPClient
4444
// FailApplySendParams is returned when updating the send config failed
4545
FailApplySendParams
46+
// FailHandshake is returned when the initial HELLO handshake returned an error
47+
FailHandshake
4648
)
4749

4850
func fail(failureID failures.FailureID, err error, v ...interface{}) failure {
49-
messages := map[int]string{
50-
int(FailGetSMTPClient): "error getting SMTP client",
51-
int(FailConnectToServer): "error connecting to server",
52-
int(FailCreateSMTPClient): "error creating smtp client",
53-
int(FailEnableStartTLS): "error enabling StartTLS",
54-
int(FailAuthenticating): "error authenticating",
55-
int(FailAuthType): "invalid authorization method '%s'",
56-
int(FailSendRecipient): "error sending message to recipient",
57-
int(FailClosingSession): "error closing session",
58-
int(FailPlainHeader): "error writing plain header",
59-
int(FailHTMLHeader): "error writing HTML header",
60-
int(FailMultiEndHeader): "error writing multipart end header",
61-
int(FailMessageTemplate): "error applying message template",
62-
int(FailMessageRaw): "error writing message",
63-
int(FailSetSender): "error creating new message",
64-
int(FailSetRecipient): "error setting RCPT",
65-
int(FailOpenDataStream): "error creating message stream",
66-
int(FailWriteHeaders): "error writing message headers",
67-
int(FailCloseDataStream): "error closing message stream",
68-
int(FailApplySendParams): "error applying params to send config",
69-
int(FailUnknown): "an unknown error occurred",
70-
}
71-
72-
msg := messages[int(failureID)]
73-
if msg == "" {
74-
msg = messages[int(FailUnknown)]
51+
var msg string
52+
switch failureID {
53+
case FailGetSMTPClient:
54+
msg = "error getting SMTP client"
55+
case FailConnectToServer:
56+
msg = "error connecting to server"
57+
case FailCreateSMTPClient:
58+
msg = "error creating smtp client"
59+
case FailEnableStartTLS:
60+
msg = "error enabling StartTLS"
61+
case FailAuthenticating:
62+
msg = "error authenticating"
63+
case FailAuthType:
64+
msg = "invalid authorization method '%s'"
65+
case FailSendRecipient:
66+
msg = "error sending message to recipient"
67+
case FailClosingSession:
68+
msg = "error closing session"
69+
case FailPlainHeader:
70+
msg = "error writing plain header"
71+
case FailHTMLHeader:
72+
msg = "error writing HTML header"
73+
case FailMultiEndHeader:
74+
msg = "error writing multipart end header"
75+
case FailMessageTemplate:
76+
msg = "error applying message template"
77+
case FailMessageRaw:
78+
msg = "error writing message"
79+
case FailSetSender:
80+
msg = "error creating new message"
81+
case FailSetRecipient:
82+
msg = "error setting RCPT"
83+
case FailOpenDataStream:
84+
msg = "error creating message stream"
85+
case FailWriteHeaders:
86+
msg = "error writing message headers"
87+
case FailCloseDataStream:
88+
msg = "error closing message stream"
89+
case FailApplySendParams:
90+
msg = "error applying params to send config"
91+
case FailHandshake:
92+
msg = "server did not accept the handshake"
93+
// case FailUnknown:
94+
default:
95+
msg = "an unknown error occurred"
7596
}
7697

7798
return failures.Wrap(msg, failureID, err, v...)

0 commit comments

Comments
 (0)