config.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package main
  2. import (
  3. "bufio"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "net"
  8. "os"
  9. "regexp"
  10. "strings"
  11. "time"
  12. "github.com/peterbourgon/ff/v3"
  13. )
  14. var (
  15. appVersion = "unknown"
  16. buildTime = "unknown"
  17. )
  18. var (
  19. flagset = flag.NewFlagSet("smtprelay", flag.ContinueOnError)
  20. // config flags
  21. logFile = flagset.String("logfile", "", "Path to logfile")
  22. logFormat = flagset.String("log_format", "default", "Log output format")
  23. logLevel = flagset.String("log_level", "info", "Minimum log level to output")
  24. hostName = flagset.String("hostname", "localhost.localdomain", "Server hostname")
  25. welcomeMsg = flagset.String("welcome_msg", "", "Welcome message for SMTP session")
  26. listenStr = flagset.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
  27. localCert = flagset.String("local_cert", "", "SSL certificate for STARTTLS/TLS")
  28. localKey = flagset.String("local_key", "", "SSL private key for STARTTLS/TLS")
  29. localForceTLS = flagset.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)")
  30. readTimeoutStr = flagset.String("read_timeout", "60s", "Socket timeout for read operations")
  31. writeTimeoutStr = flagset.String("write_timeout", "60s", "Socket timeout for write operations")
  32. dataTimeoutStr = flagset.String("data_timeout", "5m", "Socket timeout for DATA command")
  33. maxConnections = flagset.Int("max_connections", 100, "Max concurrent connections, use -1 to disable")
  34. maxMessageSize = flagset.Int("max_message_size", 10240000, "Max message size in bytes")
  35. maxRecipients = flagset.Int("max_recipients", 100, "Max RCPT TO calls for each envelope")
  36. allowedNetsStr = flagset.String("allowed_nets", "127.0.0.0/8 ::1/128", "Networks allowed to send mails")
  37. allowedSenderStr = flagset.String("allowed_sender", "", "Regular expression for valid FROM EMail addresses")
  38. allowedRecipStr = flagset.String("allowed_recipients", "", "Regular expression for valid TO EMail addresses")
  39. allowedUsers = flagset.String("allowed_users", "", "Path to file with valid users/passwords")
  40. command = flagset.String("command", "", "Path to pipe command")
  41. remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers")
  42. strictSender = flagset.Bool("strict_sender", false, "Use only SMTP servers with Sender matches to From")
  43. // additional flags
  44. _ = flagset.String("config", "", "Path to config file (ini format)")
  45. versionInfo = flagset.Bool("version", false, "Show version information")
  46. // internal
  47. listenAddrs = []protoAddr{}
  48. readTimeout time.Duration
  49. writeTimeout time.Duration
  50. dataTimeout time.Duration
  51. allowedNets = []*net.IPNet{}
  52. allowedSender *regexp.Regexp
  53. allowedRecipients *regexp.Regexp
  54. remotes = []*Remote{}
  55. )
  56. func localAuthRequired() bool {
  57. return *allowedUsers != ""
  58. }
  59. func setupAllowedNetworks() {
  60. for _, netstr := range splitstr(*allowedNetsStr, ' ') {
  61. baseIP, allowedNet, err := net.ParseCIDR(netstr)
  62. if err != nil {
  63. log.Fatal().
  64. Str("netstr", netstr).
  65. Err(err).
  66. Msg("Invalid CIDR notation in allowed_nets")
  67. }
  68. // Reject any network specification where any host bits are set,
  69. // meaning the address refers to a host and not a network.
  70. if !allowedNet.IP.Equal(baseIP) {
  71. log.Fatal().
  72. Str("given_net", netstr).
  73. Str("proper_net", allowedNet.String()).
  74. Msg("Invalid network in allowed_nets (host bits set)")
  75. }
  76. allowedNets = append(allowedNets, allowedNet)
  77. }
  78. }
  79. func setupAllowedPatterns() {
  80. var err error
  81. if *allowedSenderStr != "" {
  82. allowedSender, err = regexp.Compile(*allowedSenderStr)
  83. if err != nil {
  84. log.Fatal().
  85. Str("allowed_sender", *allowedSenderStr).
  86. Err(err).
  87. Msg("allowed_sender pattern invalid")
  88. }
  89. }
  90. if *allowedRecipStr != "" {
  91. allowedRecipients, err = regexp.Compile(*allowedRecipStr)
  92. if err != nil {
  93. log.Fatal().
  94. Str("allowed_recipients", *allowedRecipStr).
  95. Err(err).
  96. Msg("allowed_recipients pattern invalid")
  97. }
  98. }
  99. }
  100. func setupRemotes() {
  101. logger := log.With().Str("remotes", *remotesStr).Logger()
  102. if *remotesStr != "" {
  103. for _, remoteURL := range strings.Split(*remotesStr, " ") {
  104. r, err := ParseRemote(remoteURL)
  105. if err != nil {
  106. logger.Fatal().Msg(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
  107. }
  108. remotes = append(remotes, r)
  109. }
  110. }
  111. }
  112. type protoAddr struct {
  113. protocol string
  114. address string
  115. }
  116. func splitProto(s string) protoAddr {
  117. idx := strings.Index(s, "://")
  118. if idx == -1 {
  119. return protoAddr{
  120. address: s,
  121. }
  122. }
  123. return protoAddr{
  124. protocol: s[0:idx],
  125. address: s[idx+3:],
  126. }
  127. }
  128. func setupListeners() {
  129. for _, listenAddr := range strings.Split(*listenStr, " ") {
  130. pa := splitProto(listenAddr)
  131. if localAuthRequired() && pa.protocol == "" {
  132. log.Fatal().
  133. Str("address", pa.address).
  134. Msg("Local authentication (via allowed_users file) " +
  135. "not allowed with non-TLS listener")
  136. }
  137. listenAddrs = append(listenAddrs, pa)
  138. }
  139. }
  140. func setupTimeouts() {
  141. var err error
  142. readTimeout, err = time.ParseDuration(*readTimeoutStr)
  143. if err != nil {
  144. log.Fatal().
  145. Str("read_timeout", *readTimeoutStr).
  146. Err(err).
  147. Msg("read_timeout duration string invalid")
  148. }
  149. if readTimeout.Seconds() < 1 {
  150. log.Fatal().
  151. Str("read_timeout", *readTimeoutStr).
  152. Msg("read_timeout less than one second")
  153. }
  154. writeTimeout, err = time.ParseDuration(*writeTimeoutStr)
  155. if err != nil {
  156. log.Fatal().
  157. Str("write_timeout", *writeTimeoutStr).
  158. Err(err).
  159. Msg("write_timeout duration string invalid")
  160. }
  161. if writeTimeout.Seconds() < 1 {
  162. log.Fatal().
  163. Str("write_timeout", *writeTimeoutStr).
  164. Msg("write_timeout less than one second")
  165. }
  166. dataTimeout, err = time.ParseDuration(*dataTimeoutStr)
  167. if err != nil {
  168. log.Fatal().
  169. Str("data_timeout", *dataTimeoutStr).
  170. Err(err).
  171. Msg("data_timeout duration string invalid")
  172. }
  173. if dataTimeout.Seconds() < 1 {
  174. log.Fatal().
  175. Str("data_timeout", *dataTimeoutStr).
  176. Msg("data_timeout less than one second")
  177. }
  178. }
  179. func ConfigLoad() {
  180. // use .env file if it exists
  181. if _, err := os.Stat(".env"); err == nil {
  182. if err := ff.Parse(flagset, os.Args[1:],
  183. ff.WithEnvVarPrefix("smtprelay"),
  184. ff.WithConfigFile(".env"),
  185. ff.WithConfigFileParser(ff.EnvParser),
  186. ); err != nil {
  187. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  188. os.Exit(1)
  189. }
  190. } else {
  191. // use env variables and smtprelay.ini file
  192. if err := ff.Parse(flagset, os.Args[1:],
  193. ff.WithEnvVarPrefix("smtprelay"),
  194. ff.WithConfigFileFlag("config"),
  195. ff.WithConfigFileParser(IniParser),
  196. ); err != nil {
  197. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  198. os.Exit(1)
  199. }
  200. }
  201. // Set up logging as soon as possible
  202. setupLogger()
  203. if *versionInfo {
  204. fmt.Printf("smtprelay/%s (%s)\n", appVersion, buildTime)
  205. os.Exit(0)
  206. }
  207. if *remotesStr == "" && *command == "" {
  208. log.Warn().Msg("no remotes or command set; mail will not be forwarded!")
  209. }
  210. setupAllowedNetworks()
  211. setupAllowedPatterns()
  212. setupRemotes()
  213. setupListeners()
  214. setupTimeouts()
  215. }
  216. // IniParser is a parser for config files in classic key/value style format. Each
  217. // line is tokenized as a single key/value pair. The first "=" delimited
  218. // token in the line is interpreted as the flag name, and all remaining tokens
  219. // are interpreted as the value. Any leading hyphens on the flag name are
  220. // ignored.
  221. func IniParser(r io.Reader, set func(name, value string) error) error {
  222. s := bufio.NewScanner(r)
  223. for s.Scan() {
  224. line := strings.TrimSpace(s.Text())
  225. if line == "" {
  226. continue // skip empties
  227. }
  228. if line[0] == '#' || line[0] == ';' {
  229. continue // skip comments
  230. }
  231. var (
  232. name string
  233. value string
  234. index = strings.IndexRune(line, '=')
  235. )
  236. if index < 0 {
  237. name, value = line, "true" // boolean option
  238. } else {
  239. name, value = strings.TrimSpace(line[:index]), strings.Trim(strings.TrimSpace(line[index+1:]), "\"")
  240. }
  241. if i := strings.Index(value, " #"); i >= 0 {
  242. value = strings.TrimSpace(value[:i])
  243. }
  244. if err := set(name, value); err != nil {
  245. return err
  246. }
  247. }
  248. return nil
  249. }