config.go 7.8 KB

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