File: smtp_config.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 4,432 kB
  • sloc: sh: 74; makefile: 5
file content (173 lines) | stat: -rw-r--r-- 6,302 bytes parent folder | download | duplicates (2)
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
package smtp

import (
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/nicholas-fedor/shoutrrr/pkg/format"
	"github.com/nicholas-fedor/shoutrrr/pkg/types"
	"github.com/nicholas-fedor/shoutrrr/pkg/util"
)

// Scheme is the identifying part of this service's configuration URL.
const Scheme = "smtp"

// Static errors for configuration validation.
var (
	ErrFromAddressMissing = errors.New("fromAddress missing from config URL")
	ErrToAddressMissing   = errors.New("toAddress missing from config URL")
)

// Config is the configuration needed to send e-mail notifications over SMTP.
type Config struct {
	Host            string        `desc:"SMTP server hostname or IP address"                     url:"Host"`
	Username        string        `desc:"SMTP server username"                                   url:"User" default:""`
	Password        string        `desc:"SMTP server password or hash (for OAuth2)"              url:"Pass" default:""`
	Port            uint16        `desc:"SMTP server port, common ones are 25, 465, 587 or 2525" url:"Port" default:"25"`
	FromAddress     string        `desc:"E-mail address that the mail are sent from"                                                        key:"fromaddress,from"`
	FromName        string        `desc:"Name of the sender"                                                                                key:"fromname"             optional:"yes"`
	ToAddresses     []string      `desc:"List of recipient e-mails"                                                                         key:"toaddresses,to"`
	Subject         string        `desc:"The subject of the sent mail"                                      default:"Shoutrrr Notification" key:"subject,title"`
	Auth            authType      `desc:"SMTP authentication method"                                        default:"Unknown"               key:"auth"`
	Encryption      encMethod     `desc:"Encryption method"                                                 default:"Auto"                  key:"encryption"`
	UseStartTLS     bool          `desc:"Whether to use StartTLS encryption"                                default:"Yes"                   key:"usestarttls,starttls"`
	UseHTML         bool          `desc:"Whether the message being sent is in HTML"                         default:"No"                    key:"usehtml"`
	ClientHost      string        `desc:"SMTP client hostname"                                              default:"localhost"             key:"clienthost"`
	RequireStartTLS bool          `desc:"Fail if StartTLS is enabled but unsupported"                       default:"No"                    key:"requirestarttls"`
	Timeout         time.Duration `desc:"Timeout for SMTP operations"                                       default:"10s"                   key:"timeout"`
}

// GetURL returns a URL representation of its current field values.
func (config *Config) GetURL() *url.URL {
	resolver := format.NewPropKeyResolver(config)

	return config.getURL(&resolver)
}

// SetURL updates a ServiceConfig from a URL representation of its field values.
func (config *Config) SetURL(url *url.URL) error {
	resolver := format.NewPropKeyResolver(config)

	return config.setURL(&resolver, url)
}

// getURL constructs a URL from the Config's fields using the provided resolver.
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
	configURL := &url.URL{
		User:       util.URLUserPassword(config.Username, config.Password),
		Host:       fmt.Sprintf("%s:%d", config.Host, config.Port),
		Path:       "/",
		Scheme:     Scheme,
		ForceQuery: true,
	}
	// Define primary keys in the exact order matching urlWithAllProps
	primaryKeys := []string{
		"auth",
		"clienthost",
		"encryption",
		"fromaddress",
		"fromname",
		"subject",
		"toaddresses",
		"usehtml",
		"usestarttls",
		"timeout",
	}

	queryParts := make([]string, 0, len(primaryKeys)+1)
	for _, key := range primaryKeys {
		if key == "timeout" {
			queryParts = append(
				queryParts,
				fmt.Sprintf("%s=%s", key, url.QueryEscape(config.Timeout.String())),
			)

			continue
		}

		value, err := resolver.Get(key)
		if err != nil {
			continue // Skip invalid fields
		}

		queryParts = append(queryParts, fmt.Sprintf("%s=%s", key, url.QueryEscape(value)))
	}
	// Only include requirestarttls if explicitly set to true
	if config.RequireStartTLS {
		queryParts = append(queryParts, "requirestarttls=Yes")
	}

	configURL.RawQuery = strings.Join(queryParts, "&")

	return configURL
}

// setURL updates the Config from a URL using the provided resolver.
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
	password, _ := url.User.Password()
	config.Username = url.User.Username()
	config.Password = password
	config.Host = url.Hostname()

	if port, err := strconv.ParseUint(url.Port(), 10, 16); err == nil {
		config.Port = uint16(port)
	}

	for key, vals := range url.Query() {
		if key == "timeout" {
			duration, err := time.ParseDuration(vals[0])
			if err != nil {
				return fmt.Errorf("parsing timeout parameter %q: %w", vals[0], err)
			}

			config.Timeout = duration

			continue
		}

		if err := resolver.Set(key, vals[0]); err != nil {
			return fmt.Errorf("setting query parameter %q to %q: %w", key, vals[0], err)
		}
	}

	if url.String() != "smtp://dummy@dummy.com" {
		if len(config.FromAddress) < 1 {
			return ErrFromAddressMissing
		}

		if len(config.ToAddresses) < 1 {
			return ErrToAddressMissing
		}
	}

	return nil
}

// Clone returns a copy of the config.
func (config *Config) Clone() Config {
	clone := *config
	clone.ToAddresses = make([]string, len(config.ToAddresses))
	copy(clone.ToAddresses, config.ToAddresses)

	return clone
}

// FixEmailTags replaces parsed spaces (+) in e-mail addresses with '+'.
func (config *Config) FixEmailTags() {
	config.FromAddress = strings.ReplaceAll(config.FromAddress, " ", "+")
	for i, adr := range config.ToAddresses {
		config.ToAddresses[i] = strings.ReplaceAll(adr, " ", "+")
	}
}

// Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values.
func (config *Config) Enums() map[string]types.EnumFormatter {
	return map[string]types.EnumFormatter{
		"Auth":       AuthTypes.Enum,
		"Encryption": EncMethods.Enum,
	}
}