-
-
Notifications
You must be signed in to change notification settings - Fork 53
/
client.go
1331 lines (1206 loc) · 46.2 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp"
)
const (
// DefaultPort is the default connection port to the SMTP server.
DefaultPort = 25
// DefaultPortSSL is the default connection port for SSL/TLS to the SMTP server.
DefaultPortSSL = 465
// DefaultPortTLS is the default connection port for STARTTLS to the SMTP server.
DefaultPortTLS = 587
// DefaultTimeout is the default connection timeout.
DefaultTimeout = time.Second * 15
// DefaultTLSPolicy specifies the default TLS policy for connections.
DefaultTLSPolicy = TLSMandatory
// DefaultTLSMinVersion defines the minimum TLS version to be used for secure connections.
// Nowadays TLS 1.2 is assumed be a sane default.
DefaultTLSMinVersion = tls.VersionTLS12
)
const (
// DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in
// a DSN (Delivery Status Notification).
//
// https://datatracker.ietf.org/doc/html/rfc1891#section-5.3
DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS"
// DSNMailReturnFull requests that the entire mail message is returned in any failed DSN
// (Delivery Status Notification) issued for this recipient.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3
DSNMailReturnFull DSNMailReturnOption = "FULL"
// DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the
// recipient under any condition.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER"
// DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the
// message is successfully delivered.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS"
// DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of
// a message fails.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE"
// DSNRcptNotifyDelay indicates the sender's willingness to receive "delayed" DSNs.
//
// Delayed DSNs may be issued if delivery of a message has been delayed for an unusual amount of time
// (as determined by the MTA at which the message is delayed), but the final delivery status (whether
// successful or failure) cannot be determined. The absence of the DELAY keyword in a NOTIFY parameter
// requests that a "delayed" DSN NOT be issued under any conditions.
//
// https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1
DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY"
)
type (
// DialContextFunc defines a function type for establishing a network connection using context, network
// type, and address. It is used to specify custom DialContext function.
//
// By default we use net.Dial or tls.Dial respectively.
DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
// DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested
// in a Delivery Status Notification (DSN).
//
// https://datatracker.ietf.org/doc/html/rfc1891/
DSNMailReturnOption string
// DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a
// recipient in DSNs.
//
// https://datatracker.ietf.org/doc/html/rfc1891/
DSNRcptNotifyOption string
// Option is a function type that modifies the configuration or behavior of a Client instance.
Option func(*Client) error
// Client is responsible for connecting and interacting with an SMTP server.
//
// This struct represents the go-mail client, which manages the connection, authentication, and communication
// with an SMTP server. It contains various configuration options, including connection timeouts, encryption
// settings, authentication methods, and Delivery Status Notification (DSN) preferences.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc3207#section-2
// - https://datatracker.ietf.org/doc/html/rfc8314
Client struct {
// connTimeout specifies timeout for the connection to the SMTP server.
connTimeout time.Duration
// dialContextFunc is the DialContextFunc that is used by the Client to connect to the SMTP server.
dialContextFunc DialContextFunc
// dsnRcptNotifyType represents the different types of notifications for DSN (Delivery Status Notifications)
// receipts.
dsnRcptNotifyType []string
// dsnReturnType specifies the type of Delivery Status Notification (DSN) that should be requested for an
// email.
dsnReturnType DSNMailReturnOption
// fallbackPort is used as an alternative port number in case the primary port is unavailable or
// fails to bind.
//
// The fallbackPort is only used in combination with SetTLSPortPolicy and SetSSLPort correspondingly.
fallbackPort int
// helo is the hostname used in the HELO/EHLO greeting, that is sent to the target SMTP server.
//
// helo might be different as host. This can be useful in a shared-hosting scenario.
helo string
// host is the hostname of the SMTP server we are connecting to.
host string
// isEncrypted indicates wether the Client connection is encrypted or not.
isEncrypted bool
// logger is a logger that satisfies the log.Logger interface.
logger log.Logger
// mutex is used to synchronize access to shared resources, ensuring that only one goroutine can
// modify them at a time.
mutex sync.RWMutex
// noNoop indicates that the Client should skip the "NOOP" command during the dial.
//
// This is useful for servers which delay potentially unwanted clients when they perform commands
// other than AUTH.
noNoop bool
// pass represents a password or a secret token used for the SMTP authentication.
pass string
// port specifies the network port that is used to establish the connection with the SMTP server.
port int
// requestDSN indicates wether we want to request DSN (Delivery Status Notifications).
requestDSN bool
// smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It
// satisfies the smtp.Auth interface.
//
// Unless you plan to write you own custom authentication method, it is advised to not set this manually.
// You should use one of go-mail's SMTPAuthType, instead.
smtpAuth smtp.Auth
// smtpAuthType specifies the authentication type to be used for SMTP authentication.
smtpAuthType SMTPAuthType
// smtpClient is an instance of smtp.Client used for handling the communication with the SMTP server.
smtpClient *smtp.Client
// tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol.
//
// https://datatracker.ietf.org/doc/html/rfc3207#section-2
tlspolicy TLSPolicy
// tlsconfig is a pointer to tls.Config that specifies the TLS configuration for the STARTTLS communication.
tlsconfig *tls.Config
// useDebugLog indicates whether debug level logging is enabled for the Client.
useDebugLog bool
// user represents a username used for the SMTP authentication.
user string
// useSSL indicates whether to use SSL/TLS encryption for network communication.
//
// https://datatracker.ietf.org/doc/html/rfc8314
useSSL bool
}
)
var (
// ErrInvalidPort is returned when the specified port for the SMTP connection is not valid
ErrInvalidPort = errors.New("invalid port number")
// ErrInvalidTimeout is returned when the specified timeout is zero or negative.
ErrInvalidTimeout = errors.New("timeout cannot be zero or negative")
// ErrInvalidHELO is returned when the HELO/EHLO value is invalid due to being empty.
ErrInvalidHELO = errors.New("invalid HELO/EHLO value - must not be empty")
// ErrInvalidTLSConfig is returned when the provided TLS configuration is invalid or nil.
ErrInvalidTLSConfig = errors.New("invalid TLS config")
// ErrNoHostname is returned when the hostname for the client is not provided or empty.
ErrNoHostname = errors.New("hostname for client cannot be empty")
// ErrDeadlineExtendFailed is returned when an attempt to extend the connection deadline fails.
ErrDeadlineExtendFailed = errors.New("connection deadline extension failed")
// ErrNoActiveConnection indicates that there is no active connection to the SMTP server.
ErrNoActiveConnection = errors.New("not connected to SMTP server")
// ErrServerNoUnencoded indicates that the server does not support 8BITMIME for unencoded 8-bit messages.
ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME")
// ErrInvalidDSNMailReturnOption is returned when an invalid DSNMailReturnOption is provided as argument
// to the WithDSN Option.
ErrInvalidDSNMailReturnOption = errors.New("DSN mail return option can only be HDRS or FULL")
// ErrInvalidDSNRcptNotifyOption is returned when an invalid DSNRcptNotifyOption is provided as argument
// to the WithDSN Option.
ErrInvalidDSNRcptNotifyOption = errors.New("DSN rcpt notify option can only be: NEVER, " +
"SUCCESS, FAILURE or DELAY")
// ErrInvalidDSNRcptNotifyCombination is returned when an invalid combination of DSNRcptNotifyOption is
// provided as argument to the WithDSN Option.
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
"combined with any of SUCCESS, FAILURE or DELAY")
)
// NewClient creates a new Client instance with the provided host and optional configuration Option functions.
//
// This function initializes a Client with default values, such as connection timeout, port, TLS settings,
// and the HELO/EHLO hostname. Option functions, if provided, can override the default configuration.
// It ensures that essential values, like the host, are set. An error is returned if critical defaults are unset.
//
// Parameters:
// - host: The hostname of the SMTP server to connect to.
// - opts: Optional configuration functions to override default settings.
//
// Returns:
// - A pointer to the initialized Client.
// - An error if any critical default values are missing or options fail to apply.
func NewClient(host string, opts ...Option) (*Client, error) {
c := &Client{
connTimeout: DefaultTimeout,
host: host,
port: DefaultPort,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy,
}
// Set default HELO/EHLO hostname
if err := c.setDefaultHelo(); err != nil {
return c, err
}
// Override defaults with optionally provided Option functions
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(c); err != nil {
return c, fmt.Errorf("failed to apply option: %w", err)
}
}
// Some settings in a Client cannot be empty/unset
if c.host == "" {
return c, ErrNoHostname
}
return c, nil
}
// WithPort sets the port number for the Client and overrides the default port.
//
// This function sets the specified port number for the Client, ensuring that the port number is valid
// (between 1 and 65535). If the provided port number is invalid, an error is returned.
//
// Parameters:
// - port: The port number to be used by the Client. Must be between 1 and 65535.
//
// Returns:
// - An Option function that applies the port setting to the Client.
// - An error if the port number is outside the valid range.
func WithPort(port int) Option {
return func(c *Client) error {
if port < 1 || port > 65535 {
return ErrInvalidPort
}
c.port = port
return nil
}
}
// WithTimeout sets the connection timeout for the Client and overrides the default timeout.
//
// This function configures the Client with a specified connection timeout duration. It validates that the
// provided timeout is greater than zero. If the timeout is invalid, an error is returned.
//
// Parameters:
// - timeout: The duration to be set as the connection timeout. Must be greater than zero.
//
// Returns:
// - An Option function that applies the timeout setting to the Client.
// - An error if the timeout duration is invalid.
func WithTimeout(timeout time.Duration) Option {
return func(c *Client) error {
if timeout <= 0 {
return ErrInvalidTimeout
}
c.connTimeout = timeout
return nil
}
}
// WithSSL enables implicit SSL/TLS for the Client.
//
// This function configures the Client to use implicit SSL/TLS for secure communication.
//
// Returns:
// - An Option function that enables SSL/TLS for the Client.
func WithSSL() Option {
return func(c *Client) error {
c.useSSL = true
return nil
}
}
// WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. The correct port is
// automatically set.
//
// When this option is used with NewClient, the default port 25 is overridden with port 465 for SSL/TLS connections.
// If fallback is set to true and the SSL/TLS connection fails, the Client attempts to connect on port 25 using an
// unencrypted connection. If WithPort has already been used to set a different port, that port takes precedence,
// and the automatic fallback mechanism is skipped.
//
// Parameters:
// - fallback: A boolean indicating whether to fall back to port 25 without SSL/TLS if the connection fails.
//
// Returns:
// - An Option function that enables SSL/TLS and configures the fallback mechanism for the Client.
func WithSSLPort(fallback bool) Option {
return func(c *Client) error {
c.SetSSLPort(true, fallback)
return nil
}
}
// WithDebugLog enables debug logging for the Client.
//
// This function activates debug logging, which logs incoming and outgoing communication between the
// Client and the SMTP server to os.Stderr. Be cautious when using this option, as the logs may include
// unencrypted authentication data, depending on the SMTP authentication method in use, which could
// pose a data protection risk.
//
// Returns:
// - An Option function that enables debug logging for the Client.
func WithDebugLog() Option {
return func(c *Client) error {
c.useDebugLog = true
return nil
}
}
// WithLogger defines a custom logger for the Client.
//
// This function sets a custom logger for the Client, which must satisfy the log.Logger interface. The custom
// logger is used only when debug logging is enabled. By default, log.Stdlog is used if no custom logger is provided.
//
// Parameters:
// - logger: A logger that satisfies the log.Logger interface.
//
// Returns:
// - An Option function that sets the custom logger for the Client.
func WithLogger(logger log.Logger) Option {
return func(c *Client) error {
c.logger = logger
return nil
}
}
// WithHELO sets the HELO/EHLO string used by the Client.
//
// This function configures the HELO/EHLO string sent by the Client when initiating communication
// with the SMTP server. By default, os.Hostname is used to identify the HELO/EHLO string.
//
// Parameters:
// - helo: The string to be used for the HELO/EHLO greeting. Must not be empty.
//
// Returns:
// - An Option function that sets the HELO/EHLO string for the Client.
// - An error if the provided HELO string is empty.
func WithHELO(helo string) Option {
return func(c *Client) error {
if helo == "" {
return ErrInvalidHELO
}
c.helo = helo
return nil
}
}
// WithTLSPolicy sets the TLSPolicy of the Client and overrides the DefaultTLSPolicy.
//
// This function configures the Client's TLSPolicy, specifying how the Client handles TLS for SMTP connections.
// It overrides the default policy. For best practices regarding SMTP TLS connections, it is recommended to use
// WithTLSPortPolicy instead.
//
// Parameters:
// - policy: The TLSPolicy to be applied to the Client.
//
// Returns:
// - An Option function that sets the TLSPolicy for the Client.
//
// WithTLSPortPolicy instead.
func WithTLSPolicy(policy TLSPolicy) Option {
return func(c *Client) error {
c.tlspolicy = policy
return nil
}
}
// WithTLSPortPolicy enables explicit TLS via STARTTLS for the Client using the provided TLSPolicy. The
// correct port is automatically set.
//
// When TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 is used for the connection.
// If the connection fails with TLSOpportunistic, the Client attempts to connect on port 25 using an unencrypted
// connection as a fallback. If NoTLS is specified, the Client will always use port 25.
// If WithPort has already been used to set a different port, that port takes precedence, and the automatic fallback
// mechanism is skipped.
//
// Parameters:
// - policy: The TLSPolicy to be used for STARTTLS communication.
//
// Returns:
// - An Option function that sets the TLSPortPolicy for the Client.
func WithTLSPortPolicy(policy TLSPolicy) Option {
return func(c *Client) error {
c.SetTLSPortPolicy(policy)
return nil
}
}
// WithTLSConfig sets the tls.Config for the Client and overrides the default configuration.
//
// This function configures the Client with a custom tls.Config. It overrides the default TLS settings.
// An error is returned if the provided tls.Config is nil or invalid.
//
// Parameters:
// - tlsconfig: A pointer to a tls.Config struct to be used for the Client. Must not be nil.
//
// Returns:
// - An Option function that sets the tls.Config for the Client.
// - An error if the provided tls.Config is invalid.
func WithTLSConfig(tlsconfig *tls.Config) Option {
return func(c *Client) error {
if tlsconfig == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
return nil
}
}
// WithSMTPAuth configures the Client to use the specified SMTPAuthType for SMTP authentication.
//
// This function sets the Client to use the specified SMTPAuthType for authenticating with the SMTP server.
//
// Parameters:
// - authtype: The SMTPAuthType to be used for SMTP authentication.
//
// Returns:
// - An Option function that configures the Client to use the specified SMTPAuthType.
func WithSMTPAuth(authtype SMTPAuthType) Option {
return func(c *Client) error {
c.smtpAuthType = authtype
return nil
}
}
// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the Client.
//
// This function configures the Client to use a custom SMTP authentication mechanism. The provided
// mechanism must satisfy the smtp.Auth interface.
//
// Parameters:
// - smtpAuth: The custom SMTP authentication mechanism, which must implement the smtp.Auth interface.
//
// Returns:
// - An Option function that sets the custom SMTP authentication for the Client.
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
return func(c *Client) error {
c.smtpAuth = smtpAuth
c.smtpAuthType = SMTPAuthCustom
return nil
}
}
// WithUsername sets the username that the Client will use for SMTP authentication.
//
// This function configures the Client with the specified username for SMTP authentication.
//
// Parameters:
// - username: The username to be used for SMTP authentication.
//
// Returns:
// - An Option function that sets the username for the Client.
func WithUsername(username string) Option {
return func(c *Client) error {
c.user = username
return nil
}
}
// WithPassword sets the password that the Client will use for SMTP authentication.
//
// This function configures the Client with the specified password for SMTP authentication.
//
// Parameters:
// - password: The password to be used for SMTP authentication.
//
// Returns:
// - An Option function that sets the password for the Client.
func WithPassword(password string) Option {
return func(c *Client) error {
c.pass = password
return nil
}
}
// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891.
//
// This function configures the Client to request DSN, which provides status notifications for email delivery.
// DSN is only effective if the SMTP server supports it. By default, DSNMailReturnOption is set to DSNMailReturnFull,
// and DSNRcptNotifyOption is set to DSNRcptNotifySuccess and DSNRcptNotifyFailure.
//
// Returns:
// - An Option function that enables DSN for the Client.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc1891
func WithDSN() Option {
return func(c *Client) error {
c.requestDSN = true
c.dsnReturnType = DSNMailReturnFull
c.dsnRcptNotifyType = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)}
return nil
}
}
// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891.
//
// This function configures the Client to request DSN and sets the DSNMailReturnOption to the provided value.
// DSN is only effective if the SMTP server supports it. The provided option must be either DSNMailReturnHeadersOnly
// or DSNMailReturnFull; otherwise, an error is returned.
//
// Parameters:
// - option: The DSNMailReturnOption to be used (DSNMailReturnHeadersOnly or DSNMailReturnFull).
//
// Returns:
// - An Option function that sets the DSNMailReturnOption for the Client.
// - An error if an invalid DSNMailReturnOption is provided.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc1891
func WithDSNMailReturnType(option DSNMailReturnOption) Option {
return func(c *Client) error {
switch option {
case DSNMailReturnHeadersOnly:
case DSNMailReturnFull:
default:
return ErrInvalidDSNMailReturnOption
}
c.requestDSN = true
c.dsnReturnType = option
return nil
}
}
// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891.
//
// This function configures the Client to request DSN and sets the DSNRcptNotifyOption to the provided values.
// The provided options must be valid DSNRcptNotifyOption types. If DSNRcptNotifyNever is combined with
// any other notification type (such as DSNRcptNotifySuccess, DSNRcptNotifyFailure, or DSNRcptNotifyDelay),
// an error is returned.
//
// Parameters:
// - opts: A variadic list of DSNRcptNotifyOption values (e.g., DSNRcptNotifySuccess, DSNRcptNotifyFailure).
//
// Returns:
// - An Option function that sets the DSNRcptNotifyOption for the Client.
// - An error if invalid DSNRcptNotifyOption values are provided or incompatible combinations are used.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc1891
func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option {
return func(c *Client) error {
var rcptOpts []string
var ns, nns bool
if len(opts) > 0 {
for _, opt := range opts {
switch opt {
case DSNRcptNotifyNever:
ns = true
case DSNRcptNotifySuccess:
nns = true
case DSNRcptNotifyFailure:
nns = true
case DSNRcptNotifyDelay:
nns = true
default:
return ErrInvalidDSNRcptNotifyOption
}
rcptOpts = append(rcptOpts, string(opt))
}
}
if ns && nns {
return ErrInvalidDSNRcptNotifyCombination
}
c.requestDSN = true
c.dsnRcptNotifyType = rcptOpts
return nil
}
}
// WithoutNoop indicates that the Client should skip the "NOOP" command during the dial.
//
// This option is useful for servers that delay potentially unwanted clients when they perform
// commands other than AUTH, such as Microsoft's Exchange Tarpit.
//
// Returns:
// - An Option function that configures the Client to skip the "NOOP" command.
func WithoutNoop() Option {
return func(c *Client) error {
c.noNoop = true
return nil
}
}
// WithDialContextFunc sets the provided DialContextFunc as the DialContext for connecting to the SMTP server.
//
// This function overrides the default DialContext function used by the Client when establishing a connection
// to the SMTP server with the provided DialContextFunc.
//
// Parameters:
// - dialCtxFunc: The custom DialContextFunc to be used for connecting to the SMTP server.
//
// Returns:
// - An Option function that sets the custom DialContextFunc for the Client.
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
return func(c *Client) error {
c.dialContextFunc = dialCtxFunc
return nil
}
}
// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string.
//
// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation.
//
// Returns:
// - A string representing the currently set TLSPolicy for the Client.
func (c *Client) TLSPolicy() string {
return c.tlspolicy.String()
}
// ServerAddr returns the server address that is currently set on the Client in the format "host:port".
//
// This method constructs and returns the server address using the host and port currently configured
// for the Client.
//
// Returns:
// - A string representing the server address in the format "host:port".
func (c *Client) ServerAddr() string {
return fmt.Sprintf("%s:%d", c.host, c.port)
}
// SetTLSPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy.
//
// This method allows the user to set a new TLSPolicy for the Client. For best practices regarding
// SMTP TLS connections, it is recommended to use SetTLSPortPolicy instead.
//
// Parameters:
// - policy: The TLSPolicy to be set for the Client.
func (c *Client) SetTLSPolicy(policy TLSPolicy) {
c.tlspolicy = policy
}
// SetTLSPortPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy.
// The correct port is automatically set based on the specified policy.
//
// If TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 will be used for the connection.
// If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using
// an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25.
//
// Note: If a different port has already been set using WithPort, that port takes precedence and is used
// to establish the SSL/TLS connection, skipping the automatic fallback mechanism.
//
// Parameters:
// - policy: The TLSPolicy to be set for the Client.
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
if c.port == DefaultPort {
c.port = DefaultPortTLS
if policy == TLSOpportunistic {
c.fallbackPort = DefaultPort
}
if policy == NoTLS {
c.port = DefaultPort
}
}
c.tlspolicy = policy
}
// SetSSL sets or overrides whether the Client should use implicit SSL/TLS.
//
// This method configures the Client to either enable or disable implicit SSL/TLS for secure communication.
//
// Parameters:
// - ssl: A boolean value indicating whether to enable (true) or disable (false) implicit SSL/TLS.
func (c *Client) SetSSL(ssl bool) {
c.useSSL = ssl
}
// SetSSLPort sets or overrides whether the Client should use implicit SSL/TLS with optional fallback.
// The correct port is automatically set.
//
// If ssl is set to true, the default port 25 will be overridden with port 465. If fallback is set to true
// and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using an unencrypted
// connection.
//
// Note: If a different port has already been set using WithPort, that port takes precedence and is used
// to establish the SSL/TLS connection, skipping the automatic fallback mechanism.
//
// Parameters:
// - ssl: A boolean value indicating whether to enable implicit SSL/TLS.
// - fallback: A boolean value indicating whether to enable fallback to an unencrypted connection.
func (c *Client) SetSSLPort(ssl bool, fallback bool) {
if c.port == DefaultPort {
if ssl {
c.port = DefaultPortSSL
}
c.fallbackPort = 0
if fallback {
c.fallbackPort = DefaultPort
}
}
c.useSSL = ssl
}
// SetDebugLog sets or overrides whether the Client is using debug logging. The debug logger will log incoming
// and outgoing communication between the Client and the server to os.Stderr.
//
// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using
// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use
// debug logging with caution.
//
// Parameters:
// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging.
func (c *Client) SetDebugLog(val bool) {
c.useDebugLog = val
if c.smtpClient != nil {
c.smtpClient.SetDebugLog(val)
}
}
// SetLogger sets or overrides the custom logger currently used by the Client. The logger must
// satisfy the log.Logger interface and is only utilized when debug logging is enabled on the
// Client.
//
// By default, log.Stdlog is used if no custom logger is provided.
//
// Parameters:
// - logger: A logger that satisfies the log.Logger interface to be set for the Client.
func (c *Client) SetLogger(logger log.Logger) {
c.logger = logger
if c.smtpClient != nil {
c.smtpClient.SetLogger(logger)
}
}
// SetTLSConfig sets or overrides the tls.Config currently configured for the Client with the
// given value. An error is returned if the provided tls.Config is invalid.
//
// This method ensures that the provided tls.Config is not nil before updating the Client's
// TLS configuration.
//
// Parameters:
// - tlsconfig: A pointer to the tls.Config struct to be set for the Client. Must not be nil.
//
// Returns:
// - An error if the provided tls.Config is invalid or nil.
func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error {
if tlsconfig == nil {
return ErrInvalidTLSConfig
}
c.tlsconfig = tlsconfig
return nil
}
// SetUsername sets or overrides the username that the Client will use for SMTP authentication.
//
// This method updates the username used by the Client for authenticating with the SMTP server.
//
// Parameters:
// - username: The username to be set for SMTP authentication.
func (c *Client) SetUsername(username string) {
c.user = username
}
// SetPassword sets or overrides the password that the Client will use for SMTP authentication.
//
// This method updates the password used by the Client for authenticating with the SMTP server.
//
// Parameters:
// - password: The password to be set for SMTP authentication.
func (c *Client) SetPassword(password string) {
c.pass = password
}
// SetSMTPAuth sets or overrides the SMTPAuthType currently configured on the Client for SMTP
// authentication.
//
// This method updates the authentication type used by the Client for authenticating with the
// SMTP server and resets any custom SMTP authentication mechanism.
//
// Parameters:
// - authtype: The SMTPAuthType to be set for the Client.
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) {
c.smtpAuthType = authtype
c.smtpAuth = nil
}
// SetSMTPAuthCustom sets or overrides the custom SMTP authentication mechanism currently
// configured for the Client. The provided authentication mechanism must satisfy the
// smtp.Auth interface.
//
// This method updates the authentication mechanism used by the Client for authenticating
// with the SMTP server and sets the authentication type to SMTPAuthCustom.
//
// Parameters:
// - smtpAuth: The custom SMTP authentication mechanism to be set for the Client.
func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
c.smtpAuth = smtpAuth
c.smtpAuthType = SMTPAuthCustom
}
// DialWithContext establishes a connection to the server using the provided context.Context.
//
// This function adds a deadline based on the Client's timeout to the provided context.Context
// before connecting to the server. After dialing the defined DialContextFunc and successfully
// establishing the connection, it sends the HELO/EHLO SMTP command, followed by optional
// STARTTLS and SMTP AUTH commands. If debug logging is enabled, it attaches the log.Logger.
//
// After this method is called, the Client will have an active (cancelable) connection to the
// SMTP server.
//
// Parameters:
// - dialCtx: The context.Context used to control the connection timeout and cancellation.
//
// Returns:
// - An error if the connection to the SMTP server fails or any subsequent command fails.
func (c *Client) DialWithContext(dialCtx context.Context) error {
c.mutex.Lock()
defer c.mutex.Unlock()
ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout))
defer cancel()
if c.dialContextFunc == nil {
netDialer := net.Dialer{}
c.dialContextFunc = netDialer.DialContext
if c.useSSL {
tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig}
c.isEncrypted = true
c.dialContextFunc = tlsDialer.DialContext
}
}
connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr())
if err != nil && c.fallbackPort != 0 {
// TODO: should we somehow log or append the previous error?
connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr())
}
if err != nil {
return err
}
client, err := smtp.NewClient(connection, c.host)
if err != nil {
return err
}
if client == nil {
return fmt.Errorf("SMTP client is nil")
}
c.smtpClient = client
if c.logger != nil {
c.smtpClient.SetLogger(c.logger)
}
if c.useDebugLog {
c.smtpClient.SetDebugLog(true)
}
if err = c.smtpClient.Hello(c.helo); err != nil {
return err
}
if err = c.tls(); err != nil {
return err
}
if err = c.auth(); err != nil {
return err
}
return nil
}
// Close terminates the connection to the SMTP server, returning an error if the disconnection
// fails. If the connection is already closed, this method is a no-op and disregards any error.
//
// This function checks if the Client's SMTP connection is active. If not, it simply returns
// without any action. If the connection is active, it attempts to gracefully close the
// connection using the Quit method.
//
// Returns:
// - An error if the disconnection fails; otherwise, returns nil.
func (c *Client) Close() error {
if !c.smtpClient.HasConnection() {
return nil
}
if err := c.smtpClient.Quit(); err != nil {
return fmt.Errorf("failed to close SMTP client: %w", err)
}
return nil
}
// Reset sends an SMTP RSET command to reset the state of the current SMTP session.
//
// This method checks the connection to the SMTP server and, if the connection is valid,
// it sends an RSET command to reset the session state. If the connection is invalid or
// the command fails, an error is returned.
//
// Returns:
// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil.
func (c *Client) Reset() error {
if err := c.checkConn(); err != nil {
return err
}
if err := c.smtpClient.Reset(); err != nil {
return fmt.Errorf("failed to send RSET to SMTP client: %w", err)
}
return nil
}
// DialAndSend establishes a connection to the server and sends out the provided Msg.
// It calls DialAndSendWithContext with an empty Context.Background.
//
// This method simplifies the process of connecting to the SMTP server and sending messages
// by using a default context. It prepares the messages for sending and ensures the connection
// is established before attempting to send them.
//
// Parameters:
// - messages: A variadic list of pointers to Msg objects to be sent.
//
// Returns:
// - An error if the connection fails or if sending the messages fails; otherwise, returns nil.
func (c *Client) DialAndSend(messages ...*Msg) error {
ctx := context.Background()
return c.DialAndSendWithContext(ctx, messages...)
}
// DialAndSendWithContext establishes a connection to the SMTP server using DialWithContext
// with the provided context.Context, then sends out the given Msg. After successful delivery,
// the Client will close the connection to the server.
//
// This method first attempts to connect to the SMTP server using the provided context.
// Upon successful connection, it sends the specified messages and ensures that the connection