* Add support for extra sendmail arguments * Sendmail args to exec.command should be a list * Add go-shellquote package * Use go-shellquote lib for parsing Sendmail args * Only parse if sendmail is configuredtags/v1.21.12.1
| @@ -327,6 +327,8 @@ SEND_AS_PLAIN_TEXT = false | |||||
| USE_SENDMAIL = false | USE_SENDMAIL = false | ||||
| ; Specify an alternative sendmail binary | ; Specify an alternative sendmail binary | ||||
| SENDMAIL_PATH = sendmail | SENDMAIL_PATH = sendmail | ||||
| ; Specify any extra sendmail arguments | |||||
| SENDMAIL_ARGS = | |||||
| [cache] | [cache] | ||||
| ; Either "memory", "redis", or "memcache", default is "memory" | ; Either "memory", "redis", or "memcache", default is "memory" | ||||
| @@ -209,6 +209,7 @@ func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error { | |||||
| var waitError error | var waitError error | ||||
| args := []string{"-F", from, "-i"} | args := []string{"-F", from, "-i"} | ||||
| args = append(args, setting.MailService.SendmailArgs...) | |||||
| args = append(args, to...) | args = append(args, to...) | ||||
| log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args) | log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args) | ||||
| cmd := exec.Command(setting.MailService.SendmailPath, args...) | cmd := exec.Command(setting.MailService.SendmailPath, args...) | ||||
| @@ -35,6 +35,7 @@ import ( | |||||
| "github.com/go-macaron/session" | "github.com/go-macaron/session" | ||||
| _ "github.com/go-macaron/session/redis" // redis plugin for store session | _ "github.com/go-macaron/session/redis" // redis plugin for store session | ||||
| "github.com/go-xorm/core" | "github.com/go-xorm/core" | ||||
| "github.com/kballard/go-shellquote" | |||||
| "gopkg.in/ini.v1" | "gopkg.in/ini.v1" | ||||
| "strk.kbt.io/projects/go/libravatar" | "strk.kbt.io/projects/go/libravatar" | ||||
| ) | ) | ||||
| @@ -1326,6 +1327,7 @@ type Mailer struct { | |||||
| // Sendmail sender | // Sendmail sender | ||||
| UseSendmail bool | UseSendmail bool | ||||
| SendmailPath string | SendmailPath string | ||||
| SendmailArgs []string | |||||
| } | } | ||||
| var ( | var ( | ||||
| @@ -1372,6 +1374,13 @@ func newMailService() { | |||||
| MailService.FromName = parsed.Name | MailService.FromName = parsed.Name | ||||
| MailService.FromEmail = parsed.Address | MailService.FromEmail = parsed.Address | ||||
| if MailService.UseSendmail { | |||||
| MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String()) | |||||
| if err != nil { | |||||
| log.Error(4, "Failed to parse Sendmail args: %v", CustomConf, err) | |||||
| } | |||||
| } | |||||
| log.Info("Mail Service Enabled") | log.Info("Mail Service Enabled") | ||||
| } | } | ||||
| @@ -0,0 +1,19 @@ | |||||
| Copyright (C) 2014 Kevin Ballard | |||||
| Permission is hereby granted, free of charge, to any person obtaining | |||||
| a copy of this software and associated documentation files (the "Software"), | |||||
| to deal in the Software without restriction, including without limitation | |||||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | |||||
| and/or sell copies of the Software, and to permit persons to whom the | |||||
| Software is furnished to do so, subject to the following conditions: | |||||
| The above copyright notice and this permission notice shall be included | |||||
| in all copies or substantial portions of the Software. | |||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |||||
| OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |||||
| IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |||||
| DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |||||
| TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE | |||||
| OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||||
| @@ -0,0 +1,36 @@ | |||||
| PACKAGE | |||||
| package shellquote | |||||
| import "github.com/kballard/go-shellquote" | |||||
| Shellquote provides utilities for joining/splitting strings using sh's | |||||
| word-splitting rules. | |||||
| VARIABLES | |||||
| var ( | |||||
| UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string") | |||||
| UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string") | |||||
| UnterminatedEscapeError = errors.New("Unterminated backslash-escape") | |||||
| ) | |||||
| FUNCTIONS | |||||
| func Join(args ...string) string | |||||
| Join quotes each argument and joins them with a space. If passed to | |||||
| /bin/sh, the resulting string will be split back into the original | |||||
| arguments. | |||||
| func Split(input string) (words []string, err error) | |||||
| Split splits a string according to /bin/sh's word-splitting rules. It | |||||
| supports backslash-escapes, single-quotes, and double-quotes. Notably it | |||||
| does not support the $'' style of quoting. It also doesn't attempt to | |||||
| perform any other sort of expansion, including brace expansion, shell | |||||
| expansion, or pathname expansion. | |||||
| If the given input has an unterminated quoted string or ends in a | |||||
| backslash-escape, one of UnterminatedSingleQuoteError, | |||||
| UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned. | |||||
| @@ -0,0 +1,3 @@ | |||||
| // Shellquote provides utilities for joining/splitting strings using sh's | |||||
| // word-splitting rules. | |||||
| package shellquote | |||||
| @@ -0,0 +1,102 @@ | |||||
| package shellquote | |||||
| import ( | |||||
| "bytes" | |||||
| "strings" | |||||
| "unicode/utf8" | |||||
| ) | |||||
| // Join quotes each argument and joins them with a space. | |||||
| // If passed to /bin/sh, the resulting string will be split back into the | |||||
| // original arguments. | |||||
| func Join(args ...string) string { | |||||
| var buf bytes.Buffer | |||||
| for i, arg := range args { | |||||
| if i != 0 { | |||||
| buf.WriteByte(' ') | |||||
| } | |||||
| quote(arg, &buf) | |||||
| } | |||||
| return buf.String() | |||||
| } | |||||
| const ( | |||||
| specialChars = "\\'\"`${[|&;<>()*?!" | |||||
| extraSpecialChars = " \t\n" | |||||
| prefixChars = "~" | |||||
| ) | |||||
| func quote(word string, buf *bytes.Buffer) { | |||||
| // We want to try to produce a "nice" output. As such, we will | |||||
| // backslash-escape most characters, but if we encounter a space, or if we | |||||
| // encounter an extra-special char (which doesn't work with | |||||
| // backslash-escaping) we switch over to quoting the whole word. We do this | |||||
| // with a space because it's typically easier for people to read multi-word | |||||
| // arguments when quoted with a space rather than with ugly backslashes | |||||
| // everywhere. | |||||
| origLen := buf.Len() | |||||
| if len(word) == 0 { | |||||
| // oops, no content | |||||
| buf.WriteString("''") | |||||
| return | |||||
| } | |||||
| cur, prev := word, word | |||||
| atStart := true | |||||
| for len(cur) > 0 { | |||||
| c, l := utf8.DecodeRuneInString(cur) | |||||
| cur = cur[l:] | |||||
| if strings.ContainsRune(specialChars, c) || (atStart && strings.ContainsRune(prefixChars, c)) { | |||||
| // copy the non-special chars up to this point | |||||
| if len(cur) < len(prev) { | |||||
| buf.WriteString(prev[0 : len(prev)-len(cur)-l]) | |||||
| } | |||||
| buf.WriteByte('\\') | |||||
| buf.WriteRune(c) | |||||
| prev = cur | |||||
| } else if strings.ContainsRune(extraSpecialChars, c) { | |||||
| // start over in quote mode | |||||
| buf.Truncate(origLen) | |||||
| goto quote | |||||
| } | |||||
| atStart = false | |||||
| } | |||||
| if len(prev) > 0 { | |||||
| buf.WriteString(prev) | |||||
| } | |||||
| return | |||||
| quote: | |||||
| // quote mode | |||||
| // Use single-quotes, but if we find a single-quote in the word, we need | |||||
| // to terminate the string, emit an escaped quote, and start the string up | |||||
| // again | |||||
| inQuote := false | |||||
| for len(word) > 0 { | |||||
| i := strings.IndexRune(word, '\'') | |||||
| if i == -1 { | |||||
| break | |||||
| } | |||||
| if i > 0 { | |||||
| if !inQuote { | |||||
| buf.WriteByte('\'') | |||||
| inQuote = true | |||||
| } | |||||
| buf.WriteString(word[0:i]) | |||||
| } | |||||
| word = word[i+1:] | |||||
| if inQuote { | |||||
| buf.WriteByte('\'') | |||||
| inQuote = false | |||||
| } | |||||
| buf.WriteString("\\'") | |||||
| } | |||||
| if len(word) > 0 { | |||||
| if !inQuote { | |||||
| buf.WriteByte('\'') | |||||
| } | |||||
| buf.WriteString(word) | |||||
| buf.WriteByte('\'') | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,144 @@ | |||||
| package shellquote | |||||
| import ( | |||||
| "bytes" | |||||
| "errors" | |||||
| "strings" | |||||
| "unicode/utf8" | |||||
| ) | |||||
| var ( | |||||
| UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string") | |||||
| UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string") | |||||
| UnterminatedEscapeError = errors.New("Unterminated backslash-escape") | |||||
| ) | |||||
| var ( | |||||
| splitChars = " \n\t" | |||||
| singleChar = '\'' | |||||
| doubleChar = '"' | |||||
| escapeChar = '\\' | |||||
| doubleEscapeChars = "$`\"\n\\" | |||||
| ) | |||||
| // Split splits a string according to /bin/sh's word-splitting rules. It | |||||
| // supports backslash-escapes, single-quotes, and double-quotes. Notably it does | |||||
| // not support the $'' style of quoting. It also doesn't attempt to perform any | |||||
| // other sort of expansion, including brace expansion, shell expansion, or | |||||
| // pathname expansion. | |||||
| // | |||||
| // If the given input has an unterminated quoted string or ends in a | |||||
| // backslash-escape, one of UnterminatedSingleQuoteError, | |||||
| // UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned. | |||||
| func Split(input string) (words []string, err error) { | |||||
| var buf bytes.Buffer | |||||
| words = make([]string, 0) | |||||
| for len(input) > 0 { | |||||
| // skip any splitChars at the start | |||||
| c, l := utf8.DecodeRuneInString(input) | |||||
| if strings.ContainsRune(splitChars, c) { | |||||
| input = input[l:] | |||||
| continue | |||||
| } | |||||
| var word string | |||||
| word, input, err = splitWord(input, &buf) | |||||
| if err != nil { | |||||
| return | |||||
| } | |||||
| words = append(words, word) | |||||
| } | |||||
| return | |||||
| } | |||||
| func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) { | |||||
| buf.Reset() | |||||
| raw: | |||||
| { | |||||
| cur := input | |||||
| for len(cur) > 0 { | |||||
| c, l := utf8.DecodeRuneInString(cur) | |||||
| cur = cur[l:] | |||||
| if c == singleChar { | |||||
| buf.WriteString(input[0 : len(input)-len(cur)-l]) | |||||
| input = cur | |||||
| goto single | |||||
| } else if c == doubleChar { | |||||
| buf.WriteString(input[0 : len(input)-len(cur)-l]) | |||||
| input = cur | |||||
| goto double | |||||
| } else if c == escapeChar { | |||||
| buf.WriteString(input[0 : len(input)-len(cur)-l]) | |||||
| input = cur | |||||
| goto escape | |||||
| } else if strings.ContainsRune(splitChars, c) { | |||||
| buf.WriteString(input[0 : len(input)-len(cur)-l]) | |||||
| return buf.String(), cur, nil | |||||
| } | |||||
| } | |||||
| if len(input) > 0 { | |||||
| buf.WriteString(input) | |||||
| input = "" | |||||
| } | |||||
| goto done | |||||
| } | |||||
| escape: | |||||
| { | |||||
| if len(input) == 0 { | |||||
| return "", "", UnterminatedEscapeError | |||||
| } | |||||
| c, l := utf8.DecodeRuneInString(input) | |||||
| if c == '\n' { | |||||
| // a backslash-escaped newline is elided from the output entirely | |||||
| } else { | |||||
| buf.WriteString(input[:l]) | |||||
| } | |||||
| input = input[l:] | |||||
| } | |||||
| goto raw | |||||
| single: | |||||
| { | |||||
| i := strings.IndexRune(input, singleChar) | |||||
| if i == -1 { | |||||
| return "", "", UnterminatedSingleQuoteError | |||||
| } | |||||
| buf.WriteString(input[0:i]) | |||||
| input = input[i+1:] | |||||
| goto raw | |||||
| } | |||||
| double: | |||||
| { | |||||
| cur := input | |||||
| for len(cur) > 0 { | |||||
| c, l := utf8.DecodeRuneInString(cur) | |||||
| cur = cur[l:] | |||||
| if c == doubleChar { | |||||
| buf.WriteString(input[0 : len(input)-len(cur)-l]) | |||||
| input = cur | |||||
| goto raw | |||||
| } else if c == escapeChar { | |||||
| // bash only supports certain escapes in double-quoted strings | |||||
| c2, l2 := utf8.DecodeRuneInString(cur) | |||||
| cur = cur[l2:] | |||||
| if strings.ContainsRune(doubleEscapeChars, c2) { | |||||
| buf.WriteString(input[0 : len(input)-len(cur)-l-l2]) | |||||
| if c2 == '\n' { | |||||
| // newline is special, skip the backslash entirely | |||||
| } else { | |||||
| buf.WriteRune(c2) | |||||
| } | |||||
| input = cur | |||||
| } | |||||
| } | |||||
| } | |||||
| return "", "", UnterminatedDoubleQuoteError | |||||
| } | |||||
| done: | |||||
| return buf.String(), input, nil | |||||
| } | |||||
| @@ -521,6 +521,12 @@ | |||||
| "revision": "b2c7a7da5b2995941048f60146e67702a292e468", | "revision": "b2c7a7da5b2995941048f60146e67702a292e468", | ||||
| "revisionTime": "2016-02-12T04:00:40Z" | "revisionTime": "2016-02-12T04:00:40Z" | ||||
| }, | }, | ||||
| { | |||||
| "checksumSHA1": "+IzngblnBQNh+GmsS2O7jqmzSVQ=", | |||||
| "path": "github.com/kballard/go-shellquote", | |||||
| "revision": "cd60e84ee657ff3dc51de0b4f55dd299a3e136f2", | |||||
| "revisionTime": "2017-06-19T18:30:22Z" | |||||
| }, | |||||
| { | { | ||||
| "checksumSHA1": "VJk3rOWfxEV9Ilig5lgzH1qg8Ss=", | "checksumSHA1": "VJk3rOWfxEV9Ilig5lgzH1qg8Ss=", | ||||
| "path": "github.com/keybase/go-crypto/brainpool", | "path": "github.com/keybase/go-crypto/brainpool", | ||||