diff --git a/ibooks.php b/ibooks.php deleted file mode 100644 index ddde492..0000000 --- a/ibooks.php +++ /dev/null @@ -1,8 +0,0 @@ -toArray(); -$a = $a['Books']; -foreach($a as $book) - if(substr($book['Path'],-4) == '.pdf') - echo $book['Path']." - ".$book['Name']."\n"; diff --git a/imdb b/imdb deleted file mode 100755 index 248d398..0000000 --- a/imdb +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/python - -import urllib -import argparse -from xml.etree import ElementTree -import sys - -def retrieveMovie (title): - title=urllib.quote(title.encode("utf8")) - URL="http://www.omdbapi.com/?r=xml&plot=full&t=%s" % title - xml = ElementTree.parse(urllib.urlopen(URL)) - #fall back to movie search if no movie is found - for A in xml.iter('root'): - if (A.get('response')=='False'): - print "Movie not found!" - sys.exit() - xml=xml.getroot() - printInfo(xml) - return xml - -#Search for movie and return plot for all the results -def movieSearch (title): - title=urllib.quote(title.encode("utf8")) - URL="http://www.omdbapi.com/?r=xml&s=%s" % title - xml = ElementTree.parse(urllib.urlopen(URL)) - xml=xml.getroot() - for B in xml.findall('Movie'): - apicall=retrieveMovie(B.get('Title')) - printInfo(apicall) - return xml - -def printInfo(xml): - for B in xml.findall('movie'): - print "\n%s (%s) || %s || %s\n" %(B.get('title'),B.get('year'), - B.get('runtime'),B.get('imdbRating')) - print "Director: %s\nActors: %s\n" %(B.get('director'),B.get('actors')) - print "%s\n" %(B.get('plot')) - -if __name__=='__main__': - parser=argparse.ArgumentParser(description='Command-Line Interface for the IMdB') - - parser.add_argument("-t", help="Search by title. Return first result") - parser.add_argument("-s", help="Search and return results") - - args=parser.parse_args() - choices=["None"] - try: - choices[0]=sys.argv[1] - title=sys.argv[2] - except: - parser.print_usage() - sys.exit() - - if choices[0]=="-t": - retrieveMovie(title) - else: - movieSearch(title) - - diff --git a/jaybird.sh b/jaybird.sh deleted file mode 100755 index 35a34a7..0000000 --- a/jaybird.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/expect -f - -set prompt "#" -set address [lindex $argv 0] - -spawn bluetoothctl -expect -re $prompt -send "power on\r" -expect "Changing power on succeeded" -expect -re $prompt -send "agent on\r" -expect -re $prompt -send "default-agent\r" -expect "Default agent request successful" -expect -re $prompt -send "connect C0:28:8D:14:7A:9E\r" -expect "Attempting to connect to C0:28:8D:14:7A:9E" -expect -re "Connection successful" -send_user "\n jaybird connected.\n" -sleep 2 -send "quit\r" -expect eof \ No newline at end of file diff --git a/kiss.sh b/kiss.sh deleted file mode 100755 index 7d920fe..0000000 Binary files a/kiss.sh and /dev/null differ diff --git a/mfd.sh b/mfd.sh deleted file mode 100755 index 96d8243..0000000 --- a/mfd.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash -#echo Mediafire Download Script using Plowdown + Axel -#echo Usage mfd URL [num] -url=$1 -if [ ${url:0:4} != http ]; then - url="http://$url" -fi -#echo ${url:0:4} -echo Downloading $url -plowdown -p mtvz --run-download="axel -n 20 -a %url" $url -#Plowdown using axel and 10 threads to download the url - diff --git a/mongo-cleanup.sh b/mongo-cleanup.sh deleted file mode 100755 index 41379bb..0000000 --- a/mongo-cleanup.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -sudo rm /var/lib/mongodb/mongod.lock -sudo -u mongodb mongod -f /etc/mongodb.conf --repair -sudo start mongodb -sudo status mongodb diff --git a/pass.php b/pass.php deleted file mode 100755 index c7fe747..0000000 --- a/pass.php +++ /dev/null @@ -1,21 +0,0 @@ -#! /usr/bin/php -= 3: from io import StringIO import urllib.request urllib23 = urllib.request + import configparser as ConfigParser else: from cStringIO import StringIO import urllib2 urllib23 = urllib2 + import ConfigParser +try: + import yara +except: + pass CHAR_WHITESPACE = 1 CHAR_DELIMITER = 2 @@ -83,13 +117,67 @@ PDF_ELEMENT_TRAILER = 4 PDF_ELEMENT_STARTXREF = 5 PDF_ELEMENT_MALFORMED = 6 +dumplinelength = 16 + +def PrintManual(): + manual = ''' +Manual: + +This manual is a work in progress. + +There is a free PDF analysis book: +https://blog.didierstevens.com/2010/09/26/free-malicious-pdf-analysis-e-book/ + +Option -o is used to select objects by id. Provide a single id or multiple ids separated by a comma (,). + +When environment variable PDFPARSER_OPTIONS is defined, the options it defines are added implicitely to the command line arguments. +Use this to define options you want included with each use of pdf-parser.py. +Like option -O, to parse stream objects (/ObjStm). +By defining PDFPARSER_OPTIONS=-O, pdf-parser will always parse stream objects (when found). +PS: this feature is experimental. + +''' + for line in manual.split('\n'): + print(textwrap.fill(line)) + #Convert 2 Bytes If Python 3 def C2BIP3(string): if sys.version_info[0] > 2: - return bytes([ord(x) for x in string]) + if type(string) == bytes: + return string + else: + return bytes([ord(x) for x in string]) else: return string +#Convert 2 String If Python 3 +def C2SIP3(bytes): + if sys.version_info[0] > 2: + return ''.join([chr(byte) for byte in bytes]) + else: + return bytes + +# CIC: Call If Callable +def CIC(expression): + if callable(expression): + return expression() + else: + return expression + +# IFF: IF Function +def IFF(expression, valueTrue, valueFalse): + if expression: + return CIC(valueTrue) + else: + return CIC(valueFalse) + +def Timestamp(epoch=None): + if epoch == None: + localTime = time.localtime() + else: + localTime = time.localtime(epoch) + return '%04d%02d%02d-%02d%02d%02d' % localTime[0:6] + def CopyWithoutWhiteSpace(content): result = [] for token in content: @@ -103,7 +191,9 @@ def Obj2Str(content): class cPDFDocument: def __init__(self, file): self.file = file - if file.lower().startswith('http://') or file.lower().startswith('https://'): + if type(file) != str: + self.infile = file + elif file.lower().startswith('http://') or file.lower().startswith('https://'): try: if sys.hexversion >= 0x020601F0: self.infile = urllib23.urlopen(file, timeout=5) @@ -232,16 +322,25 @@ class cPDFTokenizer: token = self.Token() return token + def Tokens(self): + tokens = [] + token = self.Token() + while token != None: + tokens.append(token) + token = self.Token() + return tokens + def unget(self, byte): self.ungetted.append(byte) class cPDFParser: - def __init__(self, file, verbose=False, extract=None): + def __init__(self, file, verbose=False, extract=None, objstm=None): self.context = CONTEXT_NONE self.content = [] self.oPDFTokenizer = cPDFTokenizer(file) self.verbose = verbose self.extract = extract + self.objstm = objstm def GetObject(self): while True: @@ -281,7 +380,7 @@ class cPDFParser: else: if self.context == CONTEXT_OBJ: if self.token[1] == 'endobj': - self.oPDFElementIndirectObject = cPDFElementIndirectObject(self.objectId, self.objectVersion, self.content) + self.oPDFElementIndirectObject = cPDFElementIndirectObject(self.objectId, self.objectVersion, self.content, self.objstm) self.context = CONTEXT_NONE self.content = [] return self.oPDFElementIndirectObject @@ -311,8 +410,8 @@ class cPDFParser: if IsNumeric(self.token2[1]): self.token3 = self.oPDFTokenizer.TokenIgnoreWhiteSpace() if self.token3[1] == 'obj': - self.objectId = eval(self.token[1]) - self.objectVersion = eval(self.token2[1]) + self.objectId = int(self.token[1], 10) + self.objectVersion = int(self.token2[1], 10) self.context = CONTEXT_OBJ else: self.oPDFTokenizer.unget(self.token3) @@ -332,7 +431,7 @@ class cPDFParser: elif self.token[1] == 'startxref': self.token2 = self.oPDFTokenizer.TokenIgnoreWhiteSpace() if self.token2 and IsNumeric(self.token2[1]): - return cPDFElementStartxref(eval(self.token2[1])) + return cPDFElementStartxref(int(self.token2[1], 10)) else: self.oPDFTokenizer.unget(self.token2) if self.verbose: @@ -367,6 +466,15 @@ class cPDFElementTrailer: self.type = PDF_ELEMENT_TRAILER self.content = content + def Contains(self, keyword): + data = '' + for i in range(0, len(self.content)): + if self.content[i][1] == 'stream': + break + else: + data += Canonicalize(self.content[i][1]) + return data.upper().find(keyword.upper()) != -1 + def IIf(expr, truepart, falsepart): if expr: return truepart @@ -374,11 +482,28 @@ def IIf(expr, truepart, falsepart): return falsepart class cPDFElementIndirectObject: - def __init__(self, id, version, content): + def __init__(self, id, version, content, objstm=None): self.type = PDF_ELEMENT_INDIRECT_OBJECT self.id = id self.version = version self.content = content + self.objstm = objstm + #fix stream for Ghostscript bug reported by Kurt + if self.ContainsStream(): + position = len(self.content) - 1 + if position < 0: + return + while self.content[position][0] == CHAR_WHITESPACE and position >= 0: + position -= 1 + if position < 0: + return + if self.content[position][0] != CHAR_REGULAR: + return + if self.content[position][1] == 'endstream': + return + if not self.content[position][1].endswith('endstream'): + return + self.content = self.content[0:position] + [(self.content[position][0], self.content[position][1][:-len('endstream')])] + [(self.content[position][0], 'endstream')] + self.content[position+1:] def GetType(self): content = CopyWithoutWhiteSpace(self.content) @@ -421,12 +546,20 @@ class cPDFElementIndirectObject: data += Canonicalize(self.content[i][1]) return data.upper().find(keyword.upper()) != -1 - def StreamContains(self, keyword, filter, casesensitive, regex): + def ContainsName(self, keyword): + for token in self.content: + if token[1] == 'stream': + return False + if token[0] == CHAR_DELIMITER and EqualCanonical(token[1], keyword): + return True + return False + + def StreamContains(self, keyword, filter, casesensitive, regex, overridingfilters): if not self.ContainsStream(): return False - streamData = self.Stream(filter) + streamData = self.Stream(filter, overridingfilters) if filter and streamData == 'No filters': - streamData = self.Stream(False) + streamData = self.Stream(False, overridingfilters) if regex: return re.search(keyword, streamData, IIf(casesensitive, 0, re.I)) elif casesensitive: @@ -434,7 +567,7 @@ class cPDFElementIndirectObject: else: return keyword.lower() in streamData.lower() - def Stream(self, filter=True): + def Stream(self, filter=True, overridingfilters=''): state = 'start' countDirectories = 0 data = '' @@ -464,13 +597,24 @@ class cPDFElementIndirectObject: if self.content[i][0] == CHAR_REGULAR and self.content[i][1] == 'stream': state = 'stream-whitespace' elif state == 'stream-whitespace': - if self.content[i][0] != CHAR_WHITESPACE: + if self.content[i][0] == CHAR_WHITESPACE: + whitespace = self.content[i][1] + if whitespace.startswith('\x0D\x0A') and len(whitespace) > 2: + data += whitespace[2:] + elif whitespace.startswith('\x0A') and len(whitespace) > 1: + data += whitespace[1:] + else: data += self.content[i][1] state = 'stream-concat' elif state == 'stream-concat': if self.content[i][0] == CHAR_REGULAR and self.content[i][1] == 'endstream': if filter: - return self.Decompress(data, filters) + if overridingfilters == '': + return self.Decompress(data, filters) + elif overridingfilters == 'raw': + return data + else: + return self.Decompress(data, overridingfilters.split(' ')) else: return data else: @@ -484,7 +628,7 @@ class cPDFElementIndirectObject: if EqualCanonical(filter, '/FlateDecode') or EqualCanonical(filter, '/Fl'): try: data = FlateDecode(data) - except zlib.error, e: + except zlib.error as e: message = 'FlateDecode decompress failed' if len(data) > 0 and ord(data[0]) & 0x0F != 8: message += ', unexpected compression method: %02x' % ord(data[0]) @@ -518,6 +662,30 @@ class cPDFElementIndirectObject: else: return data + def StreamYARAMatch(self, rules, decoders, decoderoptions, filter, overridingfilters): + if not self.ContainsStream(): + return None + streamData = self.Stream(filter, overridingfilters) + if filter and streamData == 'No filters': + streamData = self.Stream(False, overridingfilters) + + oDecoders = [cIdentity(streamData, None)] + for cDecoder in decoders: + try: + oDecoder = cDecoder(streamData, decoderoptions) + oDecoders.append(oDecoder) + except Exception as e: + print('Error instantiating decoder: %s' % cDecoder.name) + raise e + results = [] + for oDecoder in oDecoders: + while oDecoder.Available(): + yaraResults = rules.match(data=oDecoder.Decode()) + if yaraResults != []: + results.append([oDecoder.Name(), yaraResults]) + + return results + class cPDFElementStartxref: def __init__(self, index): self.type = PDF_ELEMENT_STARTXREF @@ -545,7 +713,7 @@ class cPDFParseDictionary: dataTrimmed = TrimLWhiteSpace(TrimRWhiteSpace(self.content)) if dataTrimmed == []: self.parsed = None - elif self.isOpenDictionary(dataTrimmed[0]) and self.isCloseDictionary(dataTrimmed[-1]): + elif self.isOpenDictionary(dataTrimmed[0]) and (self.isCloseDictionary(dataTrimmed[-1]) or self.couldBeCloseDictionary(dataTrimmed[-1])): self.parsed = self.ParseDictionary(dataTrimmed)[0] else: self.parsed = None @@ -556,6 +724,9 @@ class cPDFParseDictionary: def isCloseDictionary(self, token): return token[0] == CHAR_DELIMITER and token[1] == '>>' + def couldBeCloseDictionary(self, token): + return token[0] == CHAR_DELIMITER and token[1].rstrip().endswith('>>') + def ParseDictionary(self, tokens): state = 0 # start dictionary = [] @@ -593,6 +764,28 @@ class cPDFParseDictionary: dictionary.append((key, value)) value = [] state = 1 + elif value == [] and tokens[0][1] == '(': + value.append(tokens[0][1]) + elif value != [] and value[0] == '(' and tokens[0][1] != ')': + if tokens[0][1][0] == '%': + tokens = [tokens[0]] + cPDFTokenizer(StringIO(tokens[0][1][1:])).Tokens() + tokens[1:] + value.append('%') + else: + value.append(tokens[0][1]) + elif value != [] and value[0] == '(' and tokens[0][1] == ')': + value.append(tokens[0][1]) + balanced = 0 + for item in value: + if item == '(': + balanced += 1 + elif item == ')': + balanced -= 1 + if balanced < 0 and self.verbose: + print('todo 11: ' + repr(value)) + if balanced < 1: + dictionary.append((key, value)) + value = [] + state = 1 elif value != [] and tokens[0][1][0] == '/': dictionary.append((key, value)) key = ConditionalCanonicalize(tokens[0][1], self.nocanonicalizedoutput) @@ -602,40 +795,96 @@ class cPDFParseDictionary: value.append(ConditionalCanonicalize(tokens[0][1], self.nocanonicalizedoutput)) tokens = tokens[1:] - def retrieve(self): + def Retrieve(self): return self.parsed + def PrettyPrintSubElement(self, prefix, e): + if e[1] == []: + print('%s %s' % (prefix, e[0])) + elif type(e[1][0]) == type(''): + if len(e[1]) == 3 and IsNumeric(e[1][0]) and e[1][1] == '0' and e[1][2] == 'R': + joiner = ' ' + else: + joiner = '' + value = joiner.join(e[1]).strip() + reprValue = repr(value) + if "'" + value + "'" != reprValue: + value = reprValue + print('%s %s %s' % (prefix, e[0], value)) + else: + print('%s %s' % (prefix, e[0])) + self.PrettyPrintSub(prefix + ' ', e[1]) + def PrettyPrintSub(self, prefix, dictionary): if dictionary != None: print('%s<<' % prefix) for e in dictionary: - if e[1] == []: - print('%s %s' % (prefix, e[0])) - elif type(e[1][0]) == type(''): - value = ''.join(e[1]).strip() - reprValue = repr(value) - if "'" + value + "'" != reprValue: - value = reprValue - print('%s %s %s' % (prefix, e[0], value)) - else: - print('%s %s' % (prefix, e[0])) - self.PrettyPrintSub(prefix + ' ', e[1]) + self.PrettyPrintSubElement(prefix, e) print('%s>>' % prefix) def PrettyPrint(self, prefix): self.PrettyPrintSub(prefix, self.parsed) + def Get(self, select): + for key, value in self.parsed: + if key == select: + return value + return None + + def GetNestedSub(self, dictionary, select): + for key, value in dictionary: + if key == select: + return self.PrettyPrintSubElement('', [select, value]) + if type(value) == type([]) and len(value) > 0 and type(value[0]) == type((None,)): + result = self.GetNestedSub(value, select) + if result !=None: + return self.PrettyPrintSubElement('', [select, result]) + return None + + def GetNested(self, select): + return self.GetNestedSub(self.parsed, select) + def FormatOutput(data, raw): if raw: if type(data) == type([]): return ''.join(map(lambda x: x[1], data)) else: return data + elif sys.version_info[0] > 2: + return ascii(data) else: return repr(data) -def PrintObject(object, options): +#Fix for http://bugs.python.org/issue11395 +def StdoutWriteChunked(data): + if sys.version_info[0] > 2: + sys.stdout.buffer.write(data) + else: + while data != '': + sys.stdout.write(data[0:10000]) + try: + sys.stdout.flush() + except IOError: + return + data = data[10000:] + +def IfWIN32SetBinary(io): + if sys.platform == 'win32': + import msvcrt + msvcrt.setmode(io.fileno(), os.O_BINARY) + +def PrintOutputObject(object, options): + if options.dump == '-': + filtered = object.Stream(options.filter == True, options.overridingfilters) + if filtered == []: + filtered = '' + IfWIN32SetBinary(sys.stdout) + StdoutWriteChunked(filtered) + return + print('obj %d %d' % (object.id, object.version)) + if object.objstm != None: + print(' Containing /ObjStm: %d %d' % object.objstm) print(' Type: %s' % ConditionalCanonicalize(object.GetType(), options.nocanonicalizedoutput)) print(' Referencing: %s' % ', '.join(map(lambda x: '%s %s %s' % x, object.GetReferences()))) dataPrecedingStream = object.ContainsStream() @@ -645,6 +894,16 @@ def PrintObject(object, options): if options.debug: print(' %s' % FormatOutput(dataPrecedingStream, options.raw)) oPDFParseDictionary = cPDFParseDictionary(dataPrecedingStream, options.nocanonicalizedoutput) + if options.hash: + streamContent = object.Stream(False, options.overridingfilters) + print(' unfiltered') + print(' len: %6d md5: %s' % (len(streamContent), hashlib.md5(streamContent).hexdigest())) + print(' %s' % HexAsciiDumpLine(streamContent)) + streamContent = object.Stream(True, options.overridingfilters) + print(' filtered') + print(' len: %6d md5: %s' % (len(streamContent), hashlib.md5(streamContent).hexdigest())) + print(' %s' % HexAsciiDumpLine(streamContent)) + streamContent = None else: if options.debug or options.raw: print(' %s' % FormatOutput(object.content, options.raw)) @@ -653,14 +912,14 @@ def PrintObject(object, options): oPDFParseDictionary.PrettyPrint(' ') print('') if options.filter and not options.dump: - filtered = object.Stream() + filtered = object.Stream(overridingfilters=options.overridingfilters) if filtered == []: print(' %s' % FormatOutput(object.content, options.raw)) else: print(' %s' % FormatOutput(filtered, options.raw)) if options.content: if object.ContainsStream(): - stream = object.Stream(False) + stream = object.Stream(False, options.overridingfilters) if stream != []: print(' %s' % FormatOutput(stream, options.raw)) else: @@ -668,7 +927,7 @@ def PrintObject(object, options): if options.dump: - filtered = object.Stream(options.filter == True) + filtered = object.Stream(options.filter == True, options.overridingfilters) if filtered == []: filtered = '' try: @@ -741,8 +1000,26 @@ def ASCII85Decode(data): def ASCIIHexDecode(data): return binascii.unhexlify(''.join([c for c in data if c not in ' \t\n\r']).rstrip('>')) +# if inflating fails, we try to inflate byte per byte (sample 4da299d6e52bbb79c0ac00bad6a1d51d4d5fe42965a8d94e88a359e5277117e2) def FlateDecode(data): - return zlib.decompress(data) + try: + return zlib.decompress(C2BIP3(data)) + except: + if len(data) <= 10: + raise + oDecompress = zlib.decompressobj() + oStringIO = StringIO() + count = 0 + for byte in C2BIP3(data): + try: + oStringIO.write(oDecompress.decompress(byte)) + count += 1 + except: + break + if len(data) - count <= 2: + return oStringIO.getvalue() + else: + raise def RunLengthDecode(data): f = StringIO(data) @@ -845,19 +1122,223 @@ class LZWDecoder(object): def LZWDecode(data): return ''.join(LZWDecoder(StringIO(data)).run()) +def PrintGenerateObject(object, options, newId=None): + if newId == None: + objectId = object.id + else: + objectId = newId + dataPrecedingStream = object.ContainsStream() + if dataPrecedingStream: + if options.filter: + decompressed = object.Stream(True, options.overridingfilters) + if decompressed == 'No filters' or decompressed.startswith('Unsupported filter: '): + print(' oPDF.stream(%d, %d, %s, %s)' % (objectId, object.version, repr(object.Stream(False, options.overridingfilters).rstrip()), repr(re.sub('/Length\s+\d+', '/Length %d', FormatOutput(dataPrecedingStream, True)).strip()))) + else: + dictionary = FormatOutput(dataPrecedingStream, True) + dictionary = re.sub(r'/Length\s+\d+', '', dictionary) + dictionary = re.sub(r'/Filter\s*/[a-zA-Z0-9]+', '', dictionary) + dictionary = re.sub(r'/Filter\s*\[.+\]', '', dictionary) + dictionary = re.sub(r'^\s*<<', '', dictionary) + dictionary = re.sub(r'>>\s*$', '', dictionary) + dictionary = dictionary.strip() + print(" oPDF.stream2(%d, %d, %s, %s, 'f')" % (objectId, object.version, repr(decompressed.rstrip()), repr(dictionary))) + else: + print(' oPDF.stream(%d, %d, %s, %s)' % (objectId, object.version, repr(object.Stream(False, options.overridingfilters).rstrip()), repr(re.sub('/Length\s+\d+', '/Length %d', FormatOutput(dataPrecedingStream, True)).strip()))) + else: + print(' oPDF.indirectobject(%d, %d, %s)' % (objectId, object.version, repr(FormatOutput(object.content, True).strip()))) + +def PrintObject(object, options): + if options.generate: + PrintGenerateObject(object, options) + else: + PrintOutputObject(object, options) + +def File2Strings(filename): + try: + f = open(filename, 'r') + except: + return None + try: + return map(lambda line:line.rstrip('\n'), f.readlines()) + except: + return None + finally: + f.close() + +def ProcessAt(argument): + if argument.startswith('@'): + strings = File2Strings(argument[1:]) + if strings == None: + raise Exception('Error reading %s' % argument) + else: + return strings + else: + return [argument] + +def YARACompile(ruledata): + if ruledata.startswith('#'): + if ruledata.startswith('#h#'): + rule = binascii.a2b_hex(ruledata[3:]) + elif ruledata.startswith('#b#'): + rule = binascii.a2b_base64(ruledata[3:]) + elif ruledata.startswith('#s#'): + rule = 'rule string {strings: $a = "%s" ascii wide nocase condition: $a}' % ruledata[3:] + elif ruledata.startswith('#q#'): + rule = ruledata[3:].replace("'", '"') + else: + rule = ruledata[1:] + return yara.compile(source=rule) + else: + dFilepaths = {} + if os.path.isdir(ruledata): + for root, dirs, files in os.walk(ruledata): + for file in files: + filename = os.path.join(root, file) + dFilepaths[filename] = filename + else: + for filename in ProcessAt(ruledata): + dFilepaths[filename] = filename + return yara.compile(filepaths=dFilepaths) + +def AddDecoder(cClass): + global decoders + + decoders.append(cClass) + +class cDecoderParent(): + pass + +def GetScriptPath(): + if getattr(sys, 'frozen', False): + return os.path.dirname(sys.executable) + else: + return os.path.dirname(sys.argv[0]) + +def LoadDecoders(decoders, verbose): + if decoders == '': + return + scriptPath = GetScriptPath() + for decoder in sum(map(ProcessAt, decoders.split(',')), []): + try: + if not decoder.lower().endswith('.py'): + decoder += '.py' + if os.path.dirname(decoder) == '': + if not os.path.exists(decoder): + scriptDecoder = os.path.join(scriptPath, decoder) + if os.path.exists(scriptDecoder): + decoder = scriptDecoder + exec(open(decoder, 'r').read(), globals(), globals()) + except Exception as e: + print('Error loading decoder: %s' % decoder) + if verbose: + raise e + +class cIdentity(cDecoderParent): + name = 'Identity function decoder' + + def __init__(self, stream, options): + self.stream = stream + self.options = options + self.available = True + + def Available(self): + return self.available + + def Decode(self): + self.available = False + return self.stream + + def Name(self): + return '' + +def DecodeFunction(decoders, options, stream): + if decoders == []: + return stream + return decoders[0](stream, options.decoderoptions).Decode() + +class cDumpStream(): + def __init__(self): + self.text = '' + + def Addline(self, line): + if line != '': + self.text += line + '\n' + + def Content(self): + return self.text + +def HexDump(data): + oDumpStream = cDumpStream() + hexDump = '' + for i, b in enumerate(data): + if i % dumplinelength == 0 and hexDump != '': + oDumpStream.Addline(hexDump) + hexDump = '' + hexDump += IFF(hexDump == '', '', ' ') + '%02X' % ord(b) + oDumpStream.Addline(hexDump) + return oDumpStream.Content() + +def CombineHexAscii(hexDump, asciiDump): + if hexDump == '': + return '' + return hexDump + ' ' + (' ' * (3 * (dumplinelength - len(asciiDump)))) + asciiDump + +def HexAsciiDump(data): + oDumpStream = cDumpStream() + hexDump = '' + asciiDump = '' + for i, b in enumerate(data): + if i % dumplinelength == 0: + if hexDump != '': + oDumpStream.Addline(CombineHexAscii(hexDump, asciiDump)) + hexDump = '%08X:' % i + asciiDump = '' + hexDump+= ' %02X' % ord(b) + asciiDump += IFF(ord(b) >= 32, b, '.') + oDumpStream.Addline(CombineHexAscii(hexDump, asciiDump)) + return oDumpStream.Content() + +def HexAsciiDumpLine(data): + return HexAsciiDump(data[0:16])[10:-1] + +def ParseINIFile(): + oConfigParser = ConfigParser.ConfigParser(allow_no_value=True) + oConfigParser.optionxform = str + oConfigParser.read(os.path.join(GetScriptPath(), 'pdfid.ini')) + keywords = [] + if oConfigParser.has_section('keywords'): + for key, value in oConfigParser.items('keywords'): + if not key in keywords: + keywords.append(key) + return keywords + +def MatchObjectID(id, selection): + return str(id) in selection.split(',') + +def GetArguments(): + arguments = sys.argv[1:] + envvar = os.getenv('PDFPARSER_OPTIONS') + if envvar == None: + return arguments + return envvar.split(' ') + arguments + def Main(): """pdf-parser, use it to parse a PDF document """ + global decoders + oParser = optparse.OptionParser(usage='usage: %prog [options] pdf-file|zip-file|url\n' + __description__, version='%prog ' + __version__) + oParser.add_option('-m', '--man', action='store_true', default=False, help='Print manual') oParser.add_option('-s', '--search', help='string to search in indirect objects (except streams)') oParser.add_option('-f', '--filter', action='store_true', default=False, help='pass stream object through filters (FlateDecode, ASCIIHexDecode, ASCII85Decode, LZWDecode and RunLengthDecode only)') - oParser.add_option('-o', '--object', help='id of indirect object to select (version independent)') + oParser.add_option('-o', '--object', help='id(s) of indirect object(s) to select, use comma (,) to separate ids (version independent)') oParser.add_option('-r', '--reference', help='id of indirect object being referenced (version independent)') oParser.add_option('-e', '--elements', help='type of elements to select (cxtsi)') oParser.add_option('-w', '--raw', action='store_true', default=False, help='raw output for data and filters') oParser.add_option('-a', '--stats', action='store_true', default=False, help='display stats for pdf document') oParser.add_option('-t', '--type', help='type of indirect object to select') + oParser.add_option('-O', '--objstm', action='store_true', default=False, help='parse stream of /ObjStm objects') oParser.add_option('-v', '--verbose', action='store_true', default=False, help='display malformed PDF elements') oParser.add_option('-x', '--extract', help='filename to extract malformed content to') oParser.add_option('-H', '--hash', action='store_true', default=False, help='display hash of objects') @@ -869,7 +1350,20 @@ def Main(): oParser.add_option('--unfiltered', action='store_true', default=False, help='search in unfiltered streams') oParser.add_option('--casesensitive', action='store_true', default=False, help='case sensitive search in streams') oParser.add_option('--regex', action='store_true', default=False, help='use regex to search in streams') - (options, args) = oParser.parse_args() + oParser.add_option('--overridingfilters', type=str, default='', help='override filters with given filters (use raw for the raw stream content)') + oParser.add_option('-g', '--generate', action='store_true', default=False, help='generate a Python program that creates the parsed PDF file') + oParser.add_option('--generateembedded', type=int, default=0, help='generate a Python program that embeds the selected indirect object as a file') + oParser.add_option('-y', '--yara', help='YARA rule (or directory or @file) to check streams (can be used with option --unfiltered)') + oParser.add_option('--yarastrings', action='store_true', default=False, help='Print YARA strings') + oParser.add_option('--decoders', type=str, default='', help='decoders to load (separate decoders with a comma , ; @file supported)') + oParser.add_option('--decoderoptions', type=str, default='', help='options for the decoder') + oParser.add_option('-k', '--key', help='key to search in dictionaries') + (options, args) = oParser.parse_args(GetArguments()) + + if options.man: + oParser.print_help() + PrintManual() + return 0 if len(args) != 1: oParser.print_help() @@ -880,6 +1374,9 @@ def Main(): print(' https://DidierStevens.com') else: + decoders = [] + LoadDecoders(options.decoders, True) + oPDFParser = cPDFParser(args[0], options.verbose, options.extract) cntComment = 0 cntXref = 0 @@ -887,6 +1384,16 @@ def Main(): cntStartXref = 0 cntIndirectObject = 0 dicObjectTypes = {} + keywords = ['/JS', '/JavaScript', '/AA', '/OpenAction', '/AcroForm', '/RichMedia', '/Launch', '/EmbeddedFile', '/XFA', '/URI'] + for extrakeyword in ParseINIFile(): + if not extrakeyword in keywords: + keywords.append(extrakeyword) + +# dKeywords = {keyword: [] for keyword in keywords} +# Done for compatibility with 2.6.6 + dKeywords = {} + for keyword in keywords: + dKeywords[keyword] = [] selectComment = False selectXref = False @@ -910,19 +1417,89 @@ def Main(): return else: selectIndirectObject = True - if not options.search and not options.object and not options.reference and not options.type and not options.searchstream: + if not options.search and not options.object and not options.reference and not options.type and not options.searchstream and not options.key: selectComment = True selectXref = True selectTrailer = True selectStartXref = True + if options.search or options.key or options.reference: + selectTrailer = True if options.type == '-': optionsType = '' else: optionsType = options.type + if options.generate or options.generateembedded != 0: + savedRoot = ['1', '0', 'R'] + print('#!/usr/bin/python') + print('') + print('"""') + print('') + print('Program generated by pdf-parser.py by Didier Stevens') + print('https://DidierStevens.com') + print('Use at your own risk') + print('') + print('Input PDF file: %s' % args[0]) + print('This Python program was created on: %s' % Timestamp()) + print('') + print('"""') + print('') + print('import mPDF') + print('import sys') + print('') + print('def Main():') + print(' if len(sys.argv) != 2:') + print(" print('Usage: %s pdf-file' % sys.argv[0])") + print(' return') + print(' oPDF = mPDF.cPDF(sys.argv[1])') + + if options.generateembedded != 0: + print(" oPDF.header('1.1')") + print(r" oPDF.comment('\xd0\xd0\xd0\xd0')") + print(r" oPDF.indirectobject(1, 0, '<<\r\n /Type /Catalog\r\n /Outlines 2 0 R\r\n /Pages 3 0 R\r\n /Names << /EmbeddedFiles << /Names [(test.bin) 7 0 R] >> >>\r\n>>')") + print(r" oPDF.indirectobject(2, 0, '<<\r\n /Type /Outlines\r\n /Count 0\r\n>>')") + print(r" oPDF.indirectobject(3, 0, '<<\r\n /Type /Pages\r\n /Kids [4 0 R]\r\n /Count 1\r\n>>')") + print(r" oPDF.indirectobject(4, 0, '<<\r\n /Type /Page\r\n /Parent 3 0 R\r\n /MediaBox [0 0 612 792]\r\n /Contents 5 0 R\r\n /Resources <<\r\n /ProcSet [/PDF /Text]\r\n /Font << /F1 6 0 R >>\r\n >>\r\n>>')") + print(r" oPDF.stream(5, 0, 'BT /F1 12 Tf 70 700 Td 15 TL (This PDF document embeds file test.bin) Tj ET', '<< /Length %d >>')") + print(r" oPDF.indirectobject(6, 0, '<<\r\n /Type /Font\r\n /Subtype /Type1\r\n /Name /F1\r\n /BaseFont /Helvetica\r\n /Encoding /MacRomanEncoding\r\n>>')") + print(r" oPDF.indirectobject(7, 0, '<<\r\n /Type /Filespec\r\n /F (test.bin)\r\n /EF << /F 8 0 R >>\r\n>>')") + + if options.yara != None: + if not 'yara' in sys.modules: + print('Error: option yara requires the YARA Python module.') + return + rules = YARACompile(options.yara) + + oPDFParserOBJSTM = None while True: - object = oPDFParser.GetObject() + if oPDFParserOBJSTM == None: + object = oPDFParser.GetObject() + else: + object = oPDFParserOBJSTM.GetObject() + if object == None: + oPDFParserOBJSTM = None + object = oPDFParser.GetObject() + if options.objstm and hasattr(object, 'GetType') and EqualCanonical(object.GetType(), '/ObjStm') and object.ContainsStream(): + # parsing objects inside an /ObjStm object by extracting & parsing the stream content to create a synthesized PDF document, that is then parsed by cPDFParser + oPDFParseDictionary = cPDFParseDictionary(object.ContainsStream(), options.nocanonicalizedoutput) + numberOfObjects = int(oPDFParseDictionary.Get('/N')[0]) + offsetFirstObject = int(oPDFParseDictionary.Get('/First')[0]) + indexes = list(map(int, C2SIP3(object.Stream())[:offsetFirstObject].strip().split(' '))) + if len(indexes) % 2 != 0 or len(indexes) / 2 != numberOfObjects: + raise Exception('Error in index of /ObjStm stream') + streamObject = C2SIP3(object.Stream()[offsetFirstObject:]) + synthesizedPDF = '' + while len(indexes) > 0: + objectNumber = indexes[0] + offset = indexes[1] + indexes = indexes[2:] + if len(indexes) >= 2: + offsetNextObject = indexes[1] + else: + offsetNextObject = len(streamObject) + synthesizedPDF += '%d 0 obj\n%s\nendobj\n' % (objectNumber, streamObject[offset:offsetNextObject]) + oPDFParserOBJSTM = cPDFParser(StringIO(synthesizedPDF), options.verbose, options.extract, (object.id, object.version)) if object != None: if options.stats: if object.type == PDF_ELEMENT_COMMENT: @@ -935,38 +1512,75 @@ def Main(): cntStartXref += 1 elif object.type == PDF_ELEMENT_INDIRECT_OBJECT: cntIndirectObject += 1 - type = object.GetType() - if not type in dicObjectTypes: - dicObjectTypes[type] = [object.id] + type1 = object.GetType() + if not type1 in dicObjectTypes: + dicObjectTypes[type1] = [object.id] else: - dicObjectTypes[type].append(object.id) + dicObjectTypes[type1].append(object.id) + for keyword in dKeywords.keys(): + if object.ContainsName(keyword): + dKeywords[keyword].append(object.id) else: if object.type == PDF_ELEMENT_COMMENT and selectComment: - print('PDF Comment %s' % FormatOutput(object.comment, options.raw)) - print('') + if options.generate: + comment = object.comment[1:].rstrip() + if re.match('PDF-\d\.\d', comment): + print(" oPDF.header('%s')" % comment[4:]) + elif comment != '%EOF': + print(' oPDF.comment(%s)' % repr(comment)) + elif options.yara == None and options.generateembedded == 0: + print('PDF Comment %s' % FormatOutput(object.comment, options.raw)) + print('') elif object.type == PDF_ELEMENT_XREF and selectXref: - if options.debug: - print('xref %s' % FormatOutput(object.content, options.raw)) - else: - print('xref') - print('') + if not options.generate and options.yara == None and options.generateembedded == 0: + if options.debug: + print('xref %s' % FormatOutput(object.content, options.raw)) + else: + print('xref') + print('') elif object.type == PDF_ELEMENT_TRAILER and selectTrailer: oPDFParseDictionary = cPDFParseDictionary(object.content[1:], options.nocanonicalizedoutput) - if oPDFParseDictionary == None: - print('trailer %s' % FormatOutput(object.content, options.raw)) - else: - print('trailer') - oPDFParseDictionary.PrettyPrint(' ') - print('') + if options.generate: + result = oPDFParseDictionary.Get('/Root') + if result != None: + savedRoot = result + elif options.yara == None and options.generateembedded == 0: + if not options.search and not options.key and not options.reference or options.search and object.Contains(options.search): + if oPDFParseDictionary == None: + print('trailer %s' % FormatOutput(object.content, options.raw)) + else: + print('trailer') + oPDFParseDictionary.PrettyPrint(' ') + print('') + elif options.key: + if oPDFParseDictionary.parsed != None: + result = oPDFParseDictionary.GetNested(options.key) + if result != None: + print(result) + elif options.reference: + for key, value in oPDFParseDictionary.Retrieve(): + if value == [str(options.reference), '0', 'R']: + print('trailer') + oPDFParseDictionary.PrettyPrint(' ') elif object.type == PDF_ELEMENT_STARTXREF and selectStartXref: - print('startxref %d' % object.index) - print('') + if not options.generate and options.yara == None and options.generateembedded == 0: + print('startxref %d' % object.index) + print('') elif object.type == PDF_ELEMENT_INDIRECT_OBJECT and selectIndirectObject: if options.search: if object.Contains(options.search): PrintObject(object, options) + elif options.key: + contentDictionary = object.ContainsStream() + if not contentDictionary: + contentDictionary = object.content[1:] + oPDFParseDictionary = cPDFParseDictionary(contentDictionary, options.nocanonicalizedoutput) + if oPDFParseDictionary.parsed != None: + result = oPDFParseDictionary.GetNested(options.key) + if result != None: + print(result) elif options.object: - if object.id == eval(options.object): + if MatchObjectID(object.id, options.object): PrintObject(object, options) elif options.reference: if object.References(options.reference): @@ -980,8 +1594,23 @@ def Main(): print(' len: %d md5: %s' % (len(rawContent), hashlib.md5(rawContent).hexdigest())) print('') elif options.searchstream: - if object.StreamContains(options.searchstream, not options.unfiltered, options.casesensitive, options.regex): + if object.StreamContains(options.searchstream, not options.unfiltered, options.casesensitive, options.regex, options.overridingfilters): PrintObject(object, options) + elif options.yara != None: + results = object.StreamYARAMatch(rules, decoders, options.decoderoptions, not options.unfiltered, options.overridingfilters) + if results != None and results != []: + for result in results: + for yaraResult in result[1]: + print('YARA rule%s: %s (%s)' % (IFF(result[0] == '', '', ' (stream decoder: %s)' % result[0]), yaraResult.rule, yaraResult.namespace)) + if options.yarastrings: + for stringdata in yaraResult.strings: + print('%06x %s:' % (stringdata[0], stringdata[1])) + print(' %s' % binascii.hexlify(C2BIP3(stringdata[2]))) + print(' %s' % repr(stringdata[2])) + PrintObject(object, options) + elif options.generateembedded != 0: + if object.id == options.generateembedded: + PrintGenerateObject(object, options, 8) else: PrintObject(object, options) elif object.type == PDF_ELEMENT_MALFORMED: @@ -1003,10 +1632,19 @@ def Main(): print('Trailer: %s' % cntTrailer) print('StartXref: %s' % cntStartXref) print('Indirect object: %s' % cntIndirectObject) - names = dicObjectTypes.keys() - names.sort() - for key in names: + for key in sorted(dicObjectTypes.keys()): print(' %s %d: %s' % (key, len(dicObjectTypes[key]), ', '.join(map(lambda x: '%d' % x, dicObjectTypes[key])))) + if sum(map(len, dKeywords.values())) > 0: + print('Search keywords:') + for keyword in keywords: + if len(dKeywords[keyword]) > 0: + print(' %s %d: %s' % (keyword, len(dKeywords[keyword]), ', '.join(map(lambda x: '%d' % x, dKeywords[keyword])))) + + if options.generate or options.generateembedded != 0: + print(" oPDF.xrefAndTrailer('%s')" % ' '.join(savedRoot)) + print('') + print("if __name__ == '__main__':") + print(' Main()') def TestPythonVersion(enforceMaximumVersion=False, enforceMinimumVersion=False): if sys.version_info[0:3] > __maximum_python_version__: diff --git a/phpCB b/phpCB deleted file mode 100755 index 55cd2b9..0000000 Binary files a/phpCB and /dev/null differ diff --git a/port_list.txt b/port_list.txt deleted file mode 100644 index 779cde9..0000000 --- a/port_list.txt +++ /dev/null @@ -1,60 +0,0 @@ -Port 21 unblocked -Port 22 unblocked -Port 53 unblocked -Port 80 unblocked -Port 81 unblocked -Port 110 unblocked -Port 161 unblocked -Port 162 unblocked -Port 210 unblocked -Port 443 unblocked -Port 465 unblocked -Port 554 unblocked -Port 587 unblocked -Port 993 unblocked -Port 995 unblocked -Port 1720 unblocked -Port 1935 unblocked -Port 1985 unblocked -Port 2000 unblocked -Port 2020 unblocked -Port 2082 unblocked -Port 2083 unblocked -Port 2095 unblocked -Port 2096 unblocked -Port 2401 unblocked -Port 3230 unblocked -Port 3231 unblocked -Port 3232 unblocked -Port 3233 unblocked -Port 3234 unblocked -Port 3235 unblocked -Port 3236 unblocked -Port 3237 unblocked -Port 3238 unblocked -Port 3239 unblocked -Port 3240 unblocked -Port 3241 unblocked -Port 3242 unblocked -Port 3243 unblocked -Port 3386 unblocked -Port 4764 unblocked -Port 4765 unblocked -Port 8008 unblocked -Port 8080 unblocked -Port 8081 unblocked -Port 8085 unblocked -Port 8400 unblocked -Port 8443 unblocked -Port 9091 unblocked -Port 10098 unblocked -Port 10099 unblocked -Port 12359 unblocked -Port 15003 unblocked -Port 18000 unblocked -Port 20001 unblocked -Port 27001 unblocked -Port 47372 unblocked -Port 47373 unblocked -Port 52709 unblocked -Port 52710 unblocked diff --git a/sakis3g b/sakis3g deleted file mode 100755 index e69de29..0000000 diff --git a/sdcardfix b/sdcardfix deleted file mode 100755 index bde24ef..0000000 --- a/sdcardfix +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -sudo rmmod sdhci-pci sdhci -sudo modprobe sdhci debug_quirks2=4 -sudo modprobe sdhci-pci \ No newline at end of file diff --git a/spoj b/spoj deleted file mode 100755 index eea29d3..0000000 --- a/spoj +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -problem=$1 -g++ $1.cpp -o $1; ./$1 < $1.IN > $1.OUT -diff $1.TXT $1.OUT diff --git a/swaks b/swaks index a9fb38d..052379a 100755 --- a/swaks +++ b/swaks @@ -1,16 +1,22 @@ #!/usr/bin/perl # use 'swaks --help' to view documentation for this program -# if you want to be notified about future releases of this program, -# please send an email to updates-swaks@jetmore.net +# +# Homepage: http://jetmore.org/john/code/swaks/ +# Online Docs: http://jetmore.org/john/code/swaks/latest/doc/ref.txt +# http://jetmore.org/john/code/swaks/faq.html +# Announce List: send mail to updates-swaks@jetmore.net +# Project RSS: http://jetmore.org/john/blog/c/swaks/feed/ +# Twitter: http://www.twitter.com/SwaksSMTP use strict; +$| = 1; my($p_name) = $0 =~ m|/?([^/]+)$|; -my $p_version = "20061116.0"; +my $p_version = build_version("DEVRELEASE", '$Id$'); my $p_usage = "Usage: $p_name [--help|--version] (see --help for details)"; -my $p_cp = < +my $p_cp = <<'EOM'; + Copyright (c) 2003-2008,2010-2019 John Jetmore This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,138 +32,47 @@ my $p_cp = < \$O{option_file}, # (l)ocation of input data - 'f|from:s' => \$O{mail_from}, # envelope-(f)rom address - 't|to:s' => \$O{mail_to}, # envelope-(t)o address - 'h|helo|ehlo|lhlo:s' => \$O{mail_helo}, # (h)elo string - 's|server:s' => \$O{mail_server}, # (s)erver to use - 'p|port:s' => \$O{mail_port}, # (p)ort to use - 'protocol:s' => \$O{mail_protocol}, # protocol to use (smtp, esmtp, lmtp) - 'd|data:s' => \$O{mail_data}, # (d)ata portion ('\n' for newlines) - 'timeout:s' => \$O{timeout}, # timeout for each trans (def 30s) - 'g' => \$O{data_on_stdin}, # (g)et data on stdin - 'm' => \$O{emulate_mail}, # emulate (M)ail command - 'q|quit|quit-after=s' => \$O{quit_after}, # (q)uit after - 'n|suppress-data' => \$O{suppress_data}, # do (n)ot print data portion - 'a|auth:s' => \$O{auth}, # force auth, exit if not supported - 'au|auth-user:s' => \$O{auth_user}, # user for auth - 'ap|auth-password:s' => \$O{auth_pass}, # pass for auth - 'am|auth-map=s' => \$O{auth_map}, # auth type map - #'ahp|auth-hide-password' => \$O{auth_hidepw}, # hide passwords when possible - 'apt|auth-plaintext' => \$O{auth_showpt}, # translate base64 strings - 'ao|auth-optional:s' => \$O{auth_optional}, # auth optional (ignore failure) - 'support' => \$O{get_support}, # report capabilties - 'li|local-interface:s' => \$O{lint}, # local interface to use - 'tls' => \$O{tls}, # use TLS - 'tlso|tls-optional' => \$O{tls_optional}, # use tls if available - 'tlsc|tls-on-connect' => \$O{tls_on_connect}, # use tls if available - 'S|silent+' => \$O{silent}, # suppress output to varying degrees - 'nsf|no-strip-from' => \$O{no_strip_from}, # Don't strip From_ line from DATA - 'nth|no-hints' => \$O{no_hints}, # Don't show transaction hints - 'hr|hide-receive' => \$O{hide_receive}, # Don't show reception lines - 'hs|hide-send' => \$O{hide_send}, # Don't show sending lines - 'stl|show-time-lapse:s' => \$O{show_time_lapse}, # print lapse for send/recv - 'ndf|no-data-fixup' => \$O{no_data_fixup}, # don't touch the data - 'pipe:s' => \$O{pipe_cmd}, # command to communicate with - 'socket:s' => \$O{socket}, # unix domain socket to talk to - 'body:s' => \$O{body_822}, # the content of the body of the DATA - 'attach-type|attach:s' => \@{$O{attach_822}}, # A file to attach - 'ah|add-header:s' => \@{$O{add_header}}, # replacement for %H DATA token - 'header:s' => \@{$O{header}}, # replace header if exist, else add - 'dump' => \$O{dump_args}, # build options and dump - 'pipeline' => \$O{pipeline}, # attempt PIPELINING - 'force-getpwuid' => \$O{force_getpwuid} # use getpwuid building -f -) || exit(1); -# lists of dependencies for features -%G::dependencies = ( - auth => { name => "Basic AUTH", opt => ['MIME::Base64'], - req => [] }, - auth_cram_md5 => { name => "AUTH CRAM-MD5", req => ['Digest::MD5'] }, - auth_cram_sha1 => { name => "AUTH CRAM-SHA1", req => ['Digest::SHA1'] }, - auth_ntlm => { name => "AUTH NTLM", req => ['Authen::NTLM'] }, - auth_digest_md5 => { name => "AUTH DIGEST-MD5", - req => ['Authen::DigestMD5'] }, - dns => { name => "MX Routing", req => ['Net::DNS'] }, - tls => { name => "TLS", req => ['Net::SSLeay'] }, - pipe => { name => "Pipe Transport", req => ['IPC::Open2'] }, - socket => { name => "Socket Transport", req => ['IO::Socket'] }, - date_manip => { name => "Date Manipulation", req => ['Time::Local'] }, - hostname => { name => "Local Hostname Detection", - req => ['Sys::Hostname'] }, - hires_timing => { name => "High Resolution Timing", - req => ['Time::HiRes'] }, -); - -if ($O{get_support}) { - test_support(); - exit(0); +# Get our functional dependencies and then print and exit early if requested +load_dependencies(); +if (get_arg('get_support', \%O)) { + test_support(); + exit(0); } +# This 'synthetic' command line used for debug and reference +$G::cmdline = reconstruct_options(\%O); + # We need to fix things up a bit and set a couple of global options my $opts = process_args(\%O); -if ($G::dump_args) { - test_support(); - print "dump_args = ", $G::dump_args ? "TRUE" : "FALSE", "\n"; - print "server_only = ", $G::server_only ? "TRUE" : "FALSE", "\n"; - print "show_time_lapse = ", $G::show_time_lapse ? "TRUE" : "FALSE", "\n"; - print "show_time_hires = ", $G::show_time_hires ? "TRUE" : "FALSE", "\n"; - print "auth_showpt = ", $G::auth_showpt ? "TRUE" : "FALSE", "\n"; - print "suppress_data = ", $G::suppress_data ? "TRUE" : "FALSE", "\n"; - print "no_hints = ", $G::no_hints ? "TRUE" : "FALSE", "\n"; - print "hide_send = ", $G::hide_send ? "TRUE" : "FALSE", "\n"; - print "hide_receive = ", $G::hide_receive ? "TRUE" : "FALSE", "\n"; - print "pipeline = ", $G::pipeline ? "TRUE" : "FALSE", "\n"; - print "silent = $G::silent\n"; - print "protocol = $G::protocol\n"; - print "type = $G::link{type}\n"; - print "server = $G::link{server}\n"; - print "sockfile = $G::link{sockfile}\n"; - print "process = $G::link{process}\n"; - print "from = $opts->{from}\n"; - print "to = $opts->{to}\n"; - print "helo = $opts->{helo}\n"; - print "port = $G::link{port}\n"; - print "tls = "; - if ($G::tls) { - print "starttls (", $G::tls_optional ? 'optional' : 'required', ")\n"; - } elsif ($G::tls_on_connect) { - print "on connect (required)\n"; - } else { print "no\n"; } - print "auth = "; - if ($opts->{a_type}) { - print $G::auth_optional ? 'optional' : 'yes', " type='", - join(',', @{$opts->{a_type}}), "' ", - "user='$opts->{a_user}' pass='$opts->{a_pass}'\n"; - } else { print "no\n"; } - print "auth map = ", join("\n".' 'x19, - map { "$_ = ". - join(', ', @{$G::auth_map_t{$_}}) - } (keys %G::auth_map_t) - ), "\n"; - print "quit after = $G::quit_after\n"; - print "local int = $G::link{lint}\n"; - print "timeout = $G::link{timeout}\n"; - print "data = <<.\n$opts->{data}\n"; - exit(0); +if (scalar(keys(%G::dump_args))) { + if (my $running_state = get_running_state($opts, \%G::dump_args)) { + # --dump is intended as a debug tool for swaks internally. Always, + # unconditionally, show the user's auth password if one is given + $running_state =~ s/'%RAW_PASSWORD_STRING%'/shquote($opts->{a_pass})/ge; + print $G::trans_fh_oh $running_state; + } + exit(0); +} +elsif ($G::dump_mail) { + # if the user just wanted to generate an email body, dump it now and exit + $opts->{data} =~ s/\n\.\Z//; + print $G::trans_fh_oh $opts->{data}; + exit(0); } # we're going to abstract away the actual connection layer from the mail @@ -175,800 +90,1439 @@ teardown_link(); exit(0); sub teardown_link { - if ($G::link{type} eq 'socket-inet' || $G::link{type} eq 'socket-unix') { - # XXX need anything special for tls teardown? - close($G::link{sock}); - ptrans(11, "Connection closed with remote host."); - } elsif ($G::link{type} eq 'pipe') { - delete($SIG{PIPE}); - $SIG{CHLD} = 'IGNORE'; - close($G::link{sock}{wr}); - close($G::link{sock}{re}); - ptrans(11, "Connection closed with child process."); - } + if ($G::link{type} eq 'socket-inet' || $G::link{type} eq 'socket-unix') { + # XXX need anything special for tls teardown? + close($G::link{sock}); + ptrans(11, "Connection closed with remote host."); + } elsif ($G::link{type} eq 'pipe') { + delete($SIG{PIPE}); + $SIG{CHLD} = 'IGNORE'; + close($G::link{sock}{wr}); + close($G::link{sock}{re}); + ptrans(11, "Connection closed with child process."); + } } sub open_link { - if ($G::link{type} eq 'socket-inet') { - ptrans(11, "Trying $G::link{server}:$G::link{port}..."); - $@ = ""; - $G::link{sock} = IO::Socket::INET->new(PeerAddr => $G::link{server}, - PeerPort => $G::link{port}, Proto => 'tcp', - Timeout => $G::link{timeout}, - LocalAddr => $G::link{lint}); + if ($G::link{type} eq 'socket-inet') { + ptrans(11, 'Trying ' . $G::link{server} . ':' . $G::link{port} . '...'); + $@ = ""; - if ($@) { - ptrans(12, "Error connecting $G::link{lint} " . - "to $G::link{server}:$G::link{port}:\n\t$@"); - exit(2); - } - ptrans(11, "Connected to $G::link{server}."); - } elsif ($G::link{type} eq 'socket-unix') { - ptrans(11, "Trying $G::link{sockfile}..."); - $SIG{PIPE} = 'IGNORE'; - $@ = ""; - $G::link{sock} = IO::Socket::UNIX->new(Peer => $G::link{sockfile}, - Timeout => $G::link{timeout}); + my @extra_options = (); + push(@extra_options, "LocalAddr", $G::link{lint}) if ($G::link{lint}); + push(@extra_options, "LocalPort", $G::link{lport}) if ($G::link{lport}); - if ($@) { - ptrans(12, "Error connecting to $G::link{sockfile}:\n\t$@"); - exit(2); - } - ptrans(11, "Connected to $G::link{sockfile}."); - } elsif ($G::link{type} eq 'pipe') { - $SIG{PIPE} = 'IGNORE'; - $SIG{CHLD} = 'IGNORE'; - ptrans(11, "Trying pipe to $G::link{process}..."); - eval{ - open2($G::link{sock}{re}, $G::link{sock}{wr}, $G::link{process}); - }; - if ($@) { - ptrans(12, "Error connecting to $G::link{process}:\n\t$@"); - exit(2); - } - select((select($G::link{sock}{wr}), $| = 1)[0]); - select((select($G::link{sock}{re}), $| = 1)[0]); - ptrans(11, "Connected to $G::link{process}."); - } else { - ptrans(12, "Unknown or unimplemented connection type " . - "$G::link{type}"); - exit(3); - } + # INET6 also supports v4, so use it for everything if it's available. That + # allows the module to handle A vs AAAA records on domain lookups where the + # user hasn't set a specific ip version to be used. If INET6 isn't available + # and it's a domain that only has AAAA records, INET will just handle it like + # a bogus record and we just won't be able to connect + if (avail("ipv6")) { + if ($G::link{force_ipv6}) { + push(@extra_options, "Domain", Socket::AF_INET6() ); + } elsif ($G::link{force_ipv4}) { + push(@extra_options, "Domain", Socket::AF_INET() ); + } + + $G::link{sock} = IO::Socket::INET6->new( + PeerAddr => $G::link{server}, + PeerPort => $G::link{port}, + Proto => 'tcp', + Timeout => $G::link{timeout}, + @extra_options + ); + } else { + $G::link{sock} = IO::Socket::INET->new( + PeerAddr => $G::link{server}, + PeerPort => $G::link{port}, + Proto => 'tcp', + Timeout => $G::link{timeout}, + @extra_options + ); + } + + if ($@) { + ptrans(12, "Error connecting" . ($G::link{lint} ? " $G::link{lint}" : '') . + " to $G::link{server}:$G::link{port}:\n\t$@"); + exit(2); + } + ptrans(11, "Connected to $G::link{server}."); + } elsif ($G::link{type} eq 'socket-unix') { + ptrans(11, 'Trying ' . $G::link{sockfile} . '...'); + $SIG{PIPE} = 'IGNORE'; + $@ = ""; + $G::link{sock} = IO::Socket::UNIX->new(Peer => $G::link{sockfile}, Timeout => $G::link{timeout}); + + if ($@) { + ptrans(12, 'Error connecting to ' . $G::link{sockfile} . ":\n\t$@"); + exit(2); + } + ptrans(11, 'Connected to ' . $G::link{sockfile} . '.'); + } elsif ($G::link{type} eq 'pipe') { + $SIG{PIPE} = 'IGNORE'; + $SIG{CHLD} = 'IGNORE'; + ptrans(11, "Trying pipe to $G::link{process}..."); + eval{ open2($G::link{sock}{re}, $G::link{sock}{wr}, $G::link{process}); }; + + if ($@) { + ptrans(12, 'Error connecting to ' . $G::link{process} . ":\n\t$@"); + exit(2); + } + select((select($G::link{sock}{wr}), $| = 1)[0]); + select((select($G::link{sock}{re}), $| = 1)[0]); + ptrans(11, 'Connected to ' . $G::link{process} . '.'); + } else { + ptrans(12, 'Unknown or unimplemented connection type ' . $G::link{type}); + exit(3); + } } sub sendmail { - my $from = shift; # envelope-from - my $to = shift; # envelope-to - my $helo = shift; # who am I? - my $data = shift; # body of message (content after DATA command) - my $a_user = shift; # what user to auth with? - my $a_pass = shift; # what pass to auth with - my $a_type = shift; # what kind of auth (this must be set to to attempt) - my $ehlo = {}; # If server is esmtp, save advertised features here + my $from = shift; # envelope-from + my $to = shift; # envelope-to + my $helo = shift; # who am I? + my $data = shift; # body of message (content after DATA command) + my $a_user = shift; # what user to auth with? + my $a_pass = shift; # what pass to auth with + my $a_type = shift; # what kind of auth (this must be set to to attempt) + my $ehlo = {}; # If server is esmtp, save advertised features here - # start up tls if -tlsc specified - if ($G::tls_on_connect) { - if (start_tls()) { - ptrans(11, "TLS started w/ cipher $G::link{tls}{cipher}"); - } else { - ptrans(12, "TLS startup failed ($G::link{tls}{res})"); - exit(29); - } - } + do_smtp_proxy() if ($G::proxy{try}); - # read the server's 220 banner - do_smtp_gen(undef, '220') || do_smtp_quit(1, 21); + # start up tls if -tlsc specified + if ($G::tls_on_connect) { + if (start_tls()) { + tls_post_start(); + do_smtp_drop() if ($G::drop_after eq 'tls'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'tls'); + } else { + ptrans(12, "TLS startup failed ($G::link{tls}{res})"); + exit(29); + } + } - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'connect'); + # read the server's 220 banner. + do_smtp_gen(undef, '220') || do_smtp_quit(1, 21); + do_smtp_drop() if ($G::drop_after eq 'connect'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'connect'); - # Send a HELO string - do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 22); + # Send a HELO string + $G::drop_before_read = 1 if ($G::drop_after_send eq 'first-helo'); + do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 22); + do_smtp_drop() if ($G::drop_after eq 'first-helo'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'first-helo'); - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'first-helo'); + if ($G::xclient{before_tls}) { + xclient_try($helo, $ehlo); + } - # handle TLS here if user has requested it - if ($G::tls) { - do_smtp_quit(1, 29) if (!do_smtp_tls($ehlo) && !$G::tls_optional); - } + # handle TLS here if user has requested it + if ($G::tls) { + # 0 = tls succeeded + # 1 = tls not advertised + # 2 = tls advertised and attempted negotiations failed + # note there's some duplicate logic here (with process_args) but I think + # it's best to do as thorough a job covering the options in both places + # so as to minimize chance of options falling through the cracks + $G::drop_before_read = 1 if ($G::drop_after_send eq 'tls'); + my $result = do_smtp_tls($ehlo); + if ($result == 1) { + ptrans(12, "Host did not advertise STARTTLS"); + do_smtp_quit(1, 29) if (!$G::tls_optional); + } elsif ($result == 2) { + ptrans(12, "STARTTLS attempted but failed"); + exit(29) if ($G::tls_optional != 1); + } + } elsif ($G::tls_optional == 2 && $ehlo->{STARTTLS}) { + ptrans(12, "TLS requested, advertised, and locally unavailable. Exiting"); + do_smtp_quit(1, 29); + } + do_smtp_drop() if ($G::drop_after eq 'tls'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'tls'); - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'tls'); + if (!$G::xclient{before_tls}) { + xclient_try($helo, $ehlo); + } - #if ($G::link{tls}{active} && $ehlo->{STARTTLS}) { - if ($G::link{tls}{active} && !$G::tls_on_connect) { - # According to RFC3207, we need to forget state info and re-EHLO here - $ehlo = {}; - do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 32); - } + #if ($G::link{tls}{active} && $ehlo->{STARTTLS}) { + if ($G::link{tls}{active} && !$G::tls_on_connect) { + # According to RFC3207, we need to forget state info and re-EHLO here + $ehlo = {}; + $G::drop_before_read = 1 if ($G::drop_after_send eq 'helo'); + do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 32); + } + do_smtp_drop() if ($G::drop_after_send eq 'helo'); # haaaack. Need to use first-helo for this. Just quit here to prevent the mail from being delivered + do_smtp_drop() if ($G::drop_after eq 'helo'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'helo'); - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'helo'); + # handle auth here if user has requested it + if ($a_type) { + # 0 = auth succeeded + # 1 = auth not advertised + # 2 = auth advertised but not attempted, no matching auth types + # 3 = auth advertised but not attempted, auth not supported + # 4 = auth advertised and attempted but no type succeeded + # note there's some duplicate logic here (with process_args) but I think + # it's best to do as thorough a job covering the options in both places + # so as to minimize chance of options falling through the cracks + $G::drop_before_read = 1 if ($G::drop_after_send eq 'auth'); + my $result = do_smtp_auth($ehlo, $a_type, $a_user, $a_pass); + if ($result == 1) { + ptrans(12, "Host did not advertise authentication"); + do_smtp_quit(1, 28) if (!$G::auth_optional); + } elsif ($result == 2) { + if ($G::auth_type eq 'ANY') { + ptrans(12, "Auth not attempted, no advertised types available"); + do_smtp_quit(1, 28) if ($G::auth_optional != 1); + } else { + ptrans(12, "Auth not attempted, requested type not available"); + do_smtp_quit(1, 28) if (!$G::auth_optional); + } + } elsif ($result == 3) { + ptrans(12, "Auth advertised but not supported locally"); + do_smtp_quit(1, 28) if ($G::auth_optional != 1); + } elsif ($result == 4) { + ptrans(12, "No authentication type succeeded"); + do_smtp_quit(1, 28) if ($G::auth_optional != 1); + } + } elsif ($G::auth_optional == 2 && $ehlo->{AUTH}) { + ptrans(12, "Auth requested, advertised, and locally unavailable. Exiting"); + do_smtp_quit(1, 28); + } + do_smtp_drop() if ($G::drop_after eq 'auth'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'auth'); - # handle auth here if user has requested it - if ($a_type) { - do_smtp_quit(1, 28) if (!do_smtp_auth($ehlo, $a_type, $a_user, $a_pass) - && !$G::auth_optional); - } + # send MAIL + # 0 = mail succeeded + # 1 = prdr required but not advertised + $G::drop_before_read = 1 if ($G::drop_after_send eq 'mail'); + my $result = do_smtp_mail($ehlo, $from); # failures in this handled by smtp_mail_callback + if ($result == 1) { + ptrans(12, "Host did not advertise PRDR support"); + do_smtp_quit(1, 30); + } + do_smtp_drop() if ($G::drop_after eq 'mail'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'mail'); - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'auth'); + # send RCPT (sub handles multiple, comma-delimited recips) + $G::drop_before_read = 1 if ($G::drop_after_send eq 'rcpt'); + do_smtp_rcpt($to); # failures in this handled by smtp_rcpt_callback + # note that smtp_rcpt_callback increments + # $G::smtp_rcpt_failures at every failure. This and + # $G::smtp_rcpt_total are used after DATA for LMTP + do_smtp_drop() if ($G::drop_after eq 'rcpt'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'rcpt'); - # send MAIL - #do_smtp_gen("MAIL FROM:<$from>", '250') || do_smtp_quit(1, 23); - do_smtp_mail($from); # failures in this handled by smtp_mail_callback + # send DATA + $G::drop_before_read = 1 if ($G::drop_after_send eq 'data'); + do_smtp_gen('DATA', '354') || do_smtp_quit(1, 25); + do_smtp_drop() if ($G::drop_after eq 'data'); - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'mail'); + # send the actual data + $G::drop_before_read = 1 if ($G::drop_after_send eq 'dot'); + do_smtp_data($data, $G::suppress_data) || do_smtp_quit(1, 26); + do_smtp_drop() if ($G::drop_after eq 'dot'); - # send RCPT (sub handles multiple, comma-delimited recips - #do_smtp_rcpt($to) || do_smtp_quit(1, 24); - do_smtp_rcpt($to); # failures in this handled by smtp_rcpt_callback - # note that smtp_rcpt_callback increments - # $G::smtp_rcpt_failures at every failure. This and - # $G::smtp_rcpt_total are used after DATA for LMTP + # send QUIT + do_smtp_quit(0) || do_smtp_quit(1, 27); +} - # QUIT here if the user has asked us to do so - do_smtp_quit(1, 0) if ($G::quit_after eq 'rcpt'); +sub xclient_try { + my $helo = shift; + my $ehlo = shift; - # send DATA - do_smtp_gen('DATA', '354') || do_smtp_quit(1, 25); + if ($G::xclient{try}) { + # 0 - xclient succeeded normally + # 1 - xclient not advertised + # 2 - xclient advertised but not attempted, mismatch in requested attrs + # 3 - xclient attempted but did not succeed + $G::drop_before_read = 1 if ($G::drop_after_send eq 'xclient'); + my $result = do_smtp_xclient($ehlo); + if ($result == 1) { + ptrans(12, "Host did not advertise XCLIENT"); + do_smtp_quit(1, 33) if (!$G::xclient{optional}); + } elsif ($result == 2) { + ptrans(12, "Host did not advertise requested XCLIENT attributes"); + do_smtp_quit(1, 33) if (!$G::xclient{optional}); + } elsif ($result == 3) { + ptrans(12, "XCLIENT attempted but failed. Exiting"); + do_smtp_quit(1, 33) if ($G::xclient{optional} != 1); + } else { + do_smtp_drop() if ($G::drop_after eq 'xclient'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'xclient'); - # send the actual data - #do_smtp_gen($data, '250', undef, $G::suppress_data) || do_smtp_quit(1, 26); - # this was moved to a custom sub because the server will have a custom - # behaviour when using LMTP - do_smtp_data($data, $G::suppress_data) || do_smtp_quit(1, 26); + # re-helo if the XCLIENT command succeeded + $G::drop_before_read = 1 if ($G::drop_after_send eq 'helo'); + do_smtp_helo($helo, $ehlo, $G::protocol) || do_smtp_quit(1, 34); + do_smtp_drop() if ($G::drop_after eq 'helo'); + do_smtp_quit(1, 0) if ($G::quit_after eq 'helo'); + } + } +} - # send QUIT - do_smtp_quit(0) || do_smtp_quit(1, 27); +sub tls_post_start { + ptrans(11, "TLS started with cipher $G::link{tls}{cipher_string}"); + if ($G::link{tls}{local_cert_subject}) { + ptrans(11, "TLS local DN=\"$G::link{tls}{local_cert_subject}\""); + } else { + ptrans(11, "TLS no local certificate set"); + } + ptrans(11, "TLS peer DN=\"$G::link{tls}{cert_subject}\""); + + if ($G::tls_get_peer_cert eq 'STDOUT') { + ptrans(11, $G::link{tls}{cert_x509}); + } elsif ($G::tls_get_peer_cert) { + open(CERT, ">$G::tls_get_peer_cert") || + ptrans(12, "Couldn't open $G::tls_get_peer_cert for writing: $!"); + print CERT $G::link{tls}{cert_x509}, "\n"; + close(CERT); + } } sub start_tls { - my %t = (); # This is a convenience var to access $G::link{tls}{...} - $G::link{tls} = \%t; + my %t = (); # This is a convenience var to access $G::link{tls}{...} + $G::link{tls} = \%t; - Net::SSLeay::load_error_strings(); - Net::SSLeay::SSLeay_add_ssl_algorithms(); - Net::SSLeay::randomize(); - $t{con} = Net::SSLeay::CTX_new() || return(0); - Net::SSLeay::CTX_set_options($t{con}, &Net::SSLeay::OP_ALL); # error check - $t{ssl} = Net::SSLeay::new($t{con}) || return(0); - if ($G::link{type} eq 'pipe') { - Net::SSLeay::set_wfd($t{ssl}, fileno($G::link{sock}{wr})); # error check? - Net::SSLeay::set_rfd($t{ssl}, fileno($G::link{sock}{re})); # error check? - } else { - Net::SSLeay::set_fd($t{ssl}, fileno($G::link{sock})); # error check? - } - $t{active} = Net::SSLeay::connect($t{ssl}) == 1 ? 1 : 0; - $t{res} = Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()) - if (!$t{active}); - $t{cipher} = Net::SSLeay::get_cipher($t{ssl}); + Net::SSLeay::load_error_strings(); + Net::SSLeay::SSLeay_add_ssl_algorithms(); + Net::SSLeay::randomize(); + if (!($t{con} = Net::SSLeay::CTX_new())) { + $t{res} = "CTX_new(): " . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } - return($t{active}); + my $ctx_options = &Net::SSLeay::OP_ALL; + if (scalar(@G::tls_protocols)) { + if ($G::tls_protocols[0] =~ /^no_/i) { + foreach my $p (@G::tls_supported_protocols) { + if (grep /^no_$p$/i, @G::tls_protocols) { + no strict "refs"; + $ctx_options |= &{"Net::SSLeay::OP_NO_$p"}(); + } + } + } else { + foreach my $p (@G::tls_supported_protocols) { + if (!grep /^$p$/i, @G::tls_protocols) { + no strict "refs"; + $ctx_options |= &{"Net::SSLeay::OP_NO_$p"}(); + } + } + } + } + Net::SSLeay::CTX_set_options($t{con}, $ctx_options); + Net::SSLeay::CTX_set_verify($t{con}, 0x01, 0) if ($G::tls_verify); + + if ($G::tls_ca_path) { + my @args = ('', $G::tls_ca_path); + @args = ($G::tls_ca_path, '') if (-f $G::tls_ca_path); + if (!Net::SSLeay::CTX_load_verify_locations($t{con}, @args)) { + $t{res} = "Unable to set set CA path to (" . join(',', @args) . "): " + . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + } else { + Net::SSLeay::CTX_set_default_verify_paths($t{con}); + } + + if ($G::tls_cipher) { + if (!Net::SSLeay::CTX_set_cipher_list($t{con}, $G::tls_cipher)) { + $t{res} = "Unable to set cipher list to $G::tls_cipher: " + . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + } + if ($G::tls_cert && $G::tls_key) { + if (!Net::SSLeay::CTX_use_certificate_file($t{con}, $G::tls_cert, &Net::SSLeay::FILETYPE_PEM)) { + $t{res} = "Unable to add cert file $G::tls_cert to SSL CTX: " + . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + if (!Net::SSLeay::CTX_use_PrivateKey_file($t{con}, $G::tls_key, &Net::SSLeay::FILETYPE_PEM)) { + $t{res} = "Unable to add key file $G::tls_key to SSL CTX: " + . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + } + + if (!($t{ssl} = Net::SSLeay::new($t{con}))) { + $t{res} = "new(): " . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + + if ($G::tls_sni_hostname) { + if (!Net::SSLeay::set_tlsext_host_name($t{ssl}, $G::tls_sni_hostname)) { + $t{res} = "Unable to set SNI hostname to $G::tls_sni_hostname: " + . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + } + + if ($G::link{type} eq 'pipe') { + Net::SSLeay::set_wfd($t{ssl}, fileno($G::link{sock}{wr})); # error check? + Net::SSLeay::set_rfd($t{ssl}, fileno($G::link{sock}{re})); # error check? + } else { + Net::SSLeay::set_fd($t{ssl}, fileno($G::link{sock})); # error check? + } + + $t{active} = Net::SSLeay::connect($t{ssl}) == 1 ? 1 : 0; + if (!$t{active}) { + $t{res} = "connect(): " . Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error()); + return(0); + } + + # egrep 'define.*VERSION\b' *.h + # when adding new types here, see also the code that pushes supported values onto tls_supported_protocols + $t{version} = Net::SSLeay::version($t{ssl}); + if ($t{version} == 0x0002) { + $t{version} = "SSLv2"; # openssl/ssl2.h + } elsif ($t{version} == 0x0300) { + $t{version} = "SSLv3"; # openssl/ssl3.h + } elsif ($t{version} == 0x0301) { + $t{version} = "TLSv1"; # openssl/tls1.h + } elsif ($t{version} == 0x0302) { + $t{version} = "TLSv1.1"; # openssl/tls1.h + } elsif ($t{version} == 0x0303) { + $t{version} = "TLSv1.2"; # openssl/tls1.h + } elsif ($t{version} == 0x0304) { + $t{version} = "TLSv1.3"; # openssl/tls1.h + } elsif ($t{version} == 0xFEFF) { + $t{version} = "DTLSv1"; # openssl/dtls1.h + } elsif ($t{version} == 0xFEFD) { + $t{version} = "DTLSv1.2"; # openssl/dtls1.h + } else { + $t{version} = sprintf("UNKNOWN(0x%04X)", $t{version}); + } + $t{cipher} = Net::SSLeay::get_cipher($t{ssl}); + if (!$t{cipher}) { + $t{res} = "empty response from get_cipher()"; + return(0); + } + $t{cipher_bits} = Net::SSLeay::get_cipher_bits($t{ssl}, undef); + if (!$t{cipher_bits}) { + $t{res} = "empty response from get_cipher_bits()"; + return(0); + } + $t{cipher_string} = sprintf("%s:%s:%s", $t{version}, $t{cipher}, $t{cipher_bits}); + $t{cert} = Net::SSLeay::get_peer_certificate($t{ssl}); + if (!$t{cert}) { + $t{res} = "error response from get_peer_certificate()"; + return(0); + } + chomp($t{cert_x509} = Net::SSLeay::PEM_get_string_X509($t{cert})); + $t{cert_subject} = Net::SSLeay::X509_NAME_oneline(Net::SSLeay::X509_get_subject_name($t{cert})); + + if ($G::tls_cert && $G::tls_key) { + $t{local_cert} = Net::SSLeay::get_certificate($t{ssl}); + chomp($t{local_cert_x509} = Net::SSLeay::PEM_get_string_X509($t{local_cert})); + $t{local_cert_subject} = Net::SSLeay::X509_NAME_oneline(Net::SSLeay::X509_get_subject_name($t{local_cert})); + } + + return($t{active}); +} + +sub deprecate { + my $message = shift; + + ptrans(12, "DEPRECATION WARNING: $message"); } sub ptrans { - my $c = shift; # transaction flag - my $m = shift; # message to print - my $b = shift; # be brief in what we print - my $o = \*STDOUT; - my $f; + my $c = shift; # transaction flag + my $m = shift; # message to print + my $b = shift; # be brief in what we print + my $a = shift; # return the message in an array ref instead of printing + my $o = $G::trans_fh_oh || \*STDOUT; + my $f = ''; - return if (($G::hide_send && int($c/10) == 2) || - ($G::hide_receive && int($c/10) == 3)); + return if (($G::hide_send && int($c/10) == 2) || + ($G::hide_receive && int($c/10) == 3) || + ($G::hide_informational && $c == 11) || + ($G::hide_all)); - # global option silent controls what we echo to the terminal - # 0 - print everything - # 1 - don't show anything until you hit an error, then show everything - # received after that (done by setting option to 0 on first error) - # 2 - don't show anything but errors - # >=3 - don't print anything - if ($G::silent > 0) { - return if ($G::silent >= 3); - return if ($G::silent == 2 && $c%2 != 0); - if ($G::silent == 1) { - if ($c%2 != 0) { - return(); - } else { - $G::silent = 0; - } - } - } + # global option silent controls what we echo to the terminal + # 0 - print everything + # 1 - don't show anything until you hit an error, then show everything + # received after that (done by setting option to 0 on first error) + # 2 - don't show anything but errors + # >=3 - don't print anything + if ($G::silent > 0) { + return if ($G::silent >= 3); + return if ($G::silent == 2 && $c%2 != 0); + if ($G::silent == 1 && !$G::ptrans_seen_error) { + if ($c%2 != 0) { + return(); + } else { + $G::ptrans_seen_error = 1; + } + } + } - # 1x is program messages - # 2x is smtp send - # 3x is smtp recv - # x = 1 is info/normal - # x = 2 is error - # program info - if ($c == 11) { $f = '==='; } - # program error - elsif ($c == 12) { $f = '***'; $o = \*STDERR; } - # smtp send info - elsif ($c == 21) { $f = $G::link{tls}{active} ? ' ~>' : ' ->'; } - # smtp send error - elsif ($c == 22) { $f = $G::link{tls}{active} ? '*~>' : '**>'; } - # smtp recv info - elsif ($c == 31) { $f = $G::link{tls}{active} ? '<~ ' : '<- '; } - # smtp recv error - elsif ($c == 32) { $f = $G::link{tls}{active} ? '<~*' : '<**'; } - # something went unexpectedly - else { $c = '???'; } + # 1x is program messages + # 2x is smtp send + # 3x is smtp recv + # x = 1 is info/normal + # x = 2 is error + # x = 3 dump output + # program info + if ($c == 11) { $f = $G::no_hints_info ? '' : '==='; } + # program error + elsif ($c == 12) { $f = $G::no_hints_info ? '' : '***'; $o = $G::trans_fh_eh || \*STDERR; } + # smtp send info + elsif ($c == 21) { $f = $G::no_hints_send ? '' : ($G::link{tls}{active} ? ' ~>' : ' ->'); } + # smtp send error + elsif ($c == 22) { $f = $G::no_hints_send ? '' : ($G::link{tls}{active} ? '*~>' : '**>'); } + # smtp send dump output + elsif ($c == 23) { $f = $G::no_hints_send ? '' : ' >'; } + # smtp recv info + elsif ($c == 31) { $f = $G::no_hints_recv ? '' : ($G::link{tls}{active} ? '<~ ' : '<- '); } + # smtp recv error + elsif ($c == 32) { $f = $G::no_hints_recv ? '' : ($G::link{tls}{active} ? '<~*' : '<**'); } + # smtp recv dump output + elsif ($c == 33) { $f = $G::no_hints_recv ? '' : '< '; } + # something went unexpectedly + else { $f = '???'; } - $f .= ' '; - $f = '' if ($G::no_hints && int($c/10) != 1); + $f .= ' ' if ($f); - if ($b) { - # split to tmp list to prevent -w gripe - my @t = split(/\n/ms, $m); $m = scalar(@t) . " lines sent"; - } - $m =~ s/\n/\n$f/msg; - print $o "$f$m\n"; + if ($b) { + # split to tmp list to prevent -w gripe + my @t = split(/\n/ms, $m); $m = scalar(@t) . " lines sent"; + } + $m =~ s/\n/\n$f/msg; + + if ($a) { + $m = "$f$m"; + return([ split(/\n/, $m) ]); + } + else { + print $o "$f$m\n"; + } } sub do_smtp_quit { - my $exit = shift; - my $err = shift; + my $exit = shift; + my $err = shift; - $G::link{allow_lost_cxn} = 1; - my $r = do_smtp_gen('QUIT', '221'); - $G::link{allow_lost_cxn} = 0; + # Ugh. Because PIPELINING allows mail's and rcpt's send to be disconnected, + # and possibly with a QUIT between them, we need to set a global "we have + # told the server we quit already" flag to prevent double-quits + return(1) if ($G::link{quit_sent}); + $G::link{quit_sent} = 1; - handle_disconnect($err) if ($G::link{lost_cxn}); + $G::link{allow_lost_cxn} = 1; + my $r = do_smtp_gen('QUIT', '221'); + $G::link{allow_lost_cxn} = 0; - if ($exit) { - teardown_link(); - exit $err; - } + handle_disconnect($err) if ($G::link{lost_cxn}); - return($r); + if ($exit) { + teardown_link(); + exit $err; + } + + return($r); +} + +sub do_smtp_drop { + ptrans(11, "Dropping connection"); + exit(0); } sub do_smtp_tls { - my $e = shift; # ehlo config hash + my $e = shift; # ehlo config hash - if (!$e->{STARTTLS}) { - ptrans(12, "STARTTLS not supported"); - return $G::tls_optional ? 1 : 0; - } elsif (!do_smtp_gen("STARTTLS", '220')) { - return $G::tls_optional ? 1 : 0; - } elsif (!start_tls()) { - ptrans(12, "TLS startup failed ($G::link{tls}{res})"); - return $G::tls_optional ? 1 : 0; - } + # 0 = tls succeeded + # 1 = tls not advertised + # 2 = tls advertised and attempted negotiations failed + if (!$e->{STARTTLS}) { + return(1); + } elsif (!do_smtp_gen("STARTTLS", '220')) { + return(2); + } elsif (!start_tls()) { + ptrans(12, "TLS startup failed ($G::link{tls}{res})"); + return(2); + } + tls_post_start(); - ptrans(11, "TLS started w/ cipher $G::link{tls}{cipher}"); - return(1); + return(0); +} + +sub do_smtp_xclient { + my $e = shift; + + # 0 - xclient succeeded normally + # 1 - xclient not advertised + # 2 - xclient advertised but not attempted, mismatch in requested attrs + # 3 - xclient attempted but did not succeed + if (!$e->{XCLIENT}) { + return(1); + } + my @parts = (); + foreach my $attr (keys %{$G::xclient{attr}}) { + if (!$e->{XCLIENT}{$attr}) { + return(2) if (!$G::xclient{no_verify}); + } + } + + foreach my $string (@{$G::xclient{strings}}) { + my $str = "XCLIENT " . $string; + do_smtp_gen($str, '220') || return(3); + } + return(0); +} + +# see xtext encoding in http://tools.ietf.org/html/rfc1891 +sub to_xtext { + my $string = shift; + + return join('', map { ($_ == 0x2b || $_ == 0x3d || $_ <= 0x20 || $_ >= 0xff) + ? sprintf("+%02X", $_) + : chr($_) + } (unpack("C*", $string))); } sub do_smtp_auth { - my $e = shift; # ehlo config hash - my $at = shift; # auth type - my $au = shift; # auth user - my $ap = shift; # auth password + my $e = shift; # ehlo config hash + my $at = shift; # auth type + my $au = shift; # auth user + my $ap = shift; # auth password - # the auth_optional stuff is handled higher up, so tell the truth about - # failing here + return(1) if (!$e->{AUTH}); + return(3) if ($G::auth_unavailable); - # note that we don't have to check whether the modules are loaded here, - # that's done in the option processing - trust that an auth type - # wouldn't be in $at if we didn't have the correct tools. + my $auth_attempted = 0; # set to true if we ever attempt auth - my $auth_attempted = 0; # set to true if we ever attempt auth + foreach my $btype (@$at) { + # if server doesn't support, skip type (may change in future) + next if (!$e->{AUTH}{$btype}); - foreach my $btype (@$at) { - # if server doesn't support, skip type (may change in future) - next if (!$e->{AUTH}{$btype}); + foreach my $type (@{$G::auth_map_t{'CRAM-MD5'}}) { + if ($btype eq $type) { + return(0) if (do_smtp_auth_cram($au, $ap, $type)); + $auth_attempted = 1; + } + } + foreach my $type (@{$G::auth_map_t{'CRAM-SHA1'}}) { + if ($btype eq $type) { + return(0) if (do_smtp_auth_cram($au, $ap, $type)); + $auth_attempted = 1; + } + } + foreach my $type (@{$G::auth_map_t{'DIGEST-MD5'}}) { + if ($btype eq $type) { + return(0) if (do_smtp_auth_digest($au, $ap, $type)); + $auth_attempted = 1; + } + } + foreach my $type (@{$G::auth_map_t{'NTLM'}}) { + if ($btype eq $type) { + return(0) if (do_smtp_auth_ntlm($au, $ap, $type)); + $auth_attempted = 1; + } + } + foreach my $type (@{$G::auth_map_t{'PLAIN'}}) { + if ($btype eq $type) { + return(0) if (do_smtp_auth_plain($au, $ap, $type)); + $auth_attempted = 1; + } + } + foreach my $type (@{$G::auth_map_t{'LOGIN'}}) { + if ($btype eq $type) { + return(0) if (do_smtp_auth_login($au, $ap, $type)); + $auth_attempted = 1; + } + } + } - foreach my $type (@{$G::auth_map_t{'CRAM-MD5'}}) { - if ($btype eq $type) { - return(1) if (do_smtp_auth_cram($au, $ap, $type)); - $auth_attempted = 1; - } - } - foreach my $type (@{$G::auth_map_t{'CRAM-SHA1'}}) { - if ($btype eq $type) { - return(1) if (do_smtp_auth_cram($au, $ap, $type)); - $auth_attempted = 1; - } - } - foreach my $type (@{$G::auth_map_t{'DIGEST-MD5'}}) { - if ($btype eq $type) { - return(1) if (do_smtp_auth_digest($au, $ap, $type)); - $auth_attempted = 1; - } - } - foreach my $type (@{$G::auth_map_t{'NTLM'}}) { - if ($btype eq $type) { - return(1) if (do_smtp_auth_ntlm($au, $ap, $type)); - $auth_attempted = 1; - } - } - foreach my $type (@{$G::auth_map_t{'PLAIN'}}) { - if ($btype eq $type) { - return(1) if (do_smtp_auth_plain($au, $ap, $type)); - $auth_attempted = 1; - } - } - foreach my $type (@{$G::auth_map_t{'LOGIN'}}) { - if ($btype eq $type) { - return(1) if (do_smtp_auth_login($au, $ap, $type)); - $auth_attempted = 1; - } - } - } - - if ($auth_attempted) { - ptrans(12, "No authentication type succeeded"); - } else { - ptrans(12, "No acceptable authentication types available"); - } - return(0); + return $auth_attempted ? 4 : 2; } sub do_smtp_auth_ntlm { - my $u = shift; # auth user - my $p = shift; # auth password - my $as = shift; # auth type (since NTLM might be SPA or MSN) - my $r = ''; # will store smtp response - my $domain; - ($u,$domain) = split(/%/, $u); + my $u = shift; # auth user + my $p = shift; # auth password + my $as = shift; # auth type (since NTLM might be SPA or MSN) + my $r = ''; # will store smtp response - my $auth_string = "AUTH $as"; - do_smtp_gen($auth_string, '334') || return(0); + my $auth_string = "AUTH $as"; + do_smtp_gen($auth_string, '334') || return(0); - my $d = db64(Authen::NTLM::ntlm()); + my $d = db64(Authen::NTLM::ntlm()); - $auth_string = eb64($d); - do_smtp_gen($auth_string, '334', \$r, '', $G::auth_showpt ? "$d" : '', - $G::auth_showpt ? \&unencode_smtp : '') || return(0); + $auth_string = eb64($d); + do_smtp_gen($auth_string, '334', \$r, '', + $G::auth_showpt ? "$d" : '', + $G::auth_showpt ? \&unencode_smtp : '') || return(0); - $r =~ s/^....//; # maybe something a little better here? - Authen::NTLM::ntlm_domain($domain); - Authen::NTLM::ntlm_user($u); - Authen::NTLM::ntlm_password($p); - $d = db64(Authen::NTLM::ntlm($r)); + $r =~ s/^....//; # maybe something a little better here? + Authen::NTLM::ntlm_domain($G::auth_extras{DOMAIN}); + Authen::NTLM::ntlm_user($u); + Authen::NTLM::ntlm_password($p); + $d = db64(Authen::NTLM::ntlm($r)); - $auth_string = eb64($d); - do_smtp_gen($auth_string, '235', \$r, '', - $G::auth_showpt ? "$d" : '') || return(0); + $auth_string = eb64($d); + do_smtp_gen($auth_string, '235', \$r, '', $G::auth_showpt ? "$d" : '') || return(0); - return(1); + return(1); } sub do_smtp_auth_digest { - my $u = shift; # auth user - my $p = shift; # auth password - my $as = shift; # auth string - my $r = ''; # will store smtp response + my $u = shift; # auth user + my $p = shift; # auth password + my $as = shift; # auth string + my $r = ''; # will store smtp response + my $e = ''; # will store Authen::SASL errors + my @digest_uri = (); - my $auth_string = "AUTH $as"; - do_smtp_gen($auth_string, '334', \$r, '', '', - $G::auth_showpt ? \&unencode_smtp : '') - || return(0); + if (exists($G::auth_extras{"DMD5-SERV-TYPE"})) { + $digest_uri[0] = $G::auth_extras{"DMD5-SERV-TYPE"}; + } else { + $digest_uri[0] = 'smtp'; + } + if (exists($G::auth_extras{"DMD5-HOST"})) { + $digest_uri[1] = $G::auth_extras{"DMD5-HOST"}; + } else { + if ($G::link{type} eq 'socket-unix') { + $digest_uri[1] = $G::link{sockfile}; + $digest_uri[1] =~ s|[^a-zA-Z0-9\.\-]|-|g; + } elsif ($G::link{type} eq 'pipe') { + $digest_uri[1] = $G::link{process}; + $digest_uri[1] =~ s|[^a-zA-Z0-9\.\-]|-|g; + } else { + $digest_uri[1] = $G::link{server}; + } + } + if (exists($G::auth_extras{"DMD5-SERV-NAME"})) { + # There seems to be a hole in the Authen::SASL interface where there's + # no option to directory provide the digest-uri serv-name. But we can + # trick it into using the value we want by tacking it onto the end of host + $digest_uri[1] .= '/' . $G::auth_extras{"DMD5-SERV-NAME"}; + } - $r =~ s/^....//; # maybe something a little better here? - $r = db64($r); - my $req = Authen::DigestMD5::Request->new($r); - my $res = Authen::DigestMD5::Response->new(); - $res->got_request($req); - # XXX using link{server} here is probably a bug, but I don;t know what else - # XXX to use yet on a non-inet-socket connection - $res->set('username' => $u, 'realm' => '', - 'digest-uri' => "smtp/$G::link{server}"); - $res->add_digest(password => $p); - my $d = $res->output(); - $auth_string = eb64($d); + my $auth_string = "AUTH $as"; + do_smtp_gen($auth_string, '334', \$r, '', '', $G::auth_showpt ? \&unencode_smtp : '') + || return(0); - do_smtp_gen($auth_string, '334', \$r, '', $G::auth_showpt ? "$d" : '', - $G::auth_showpt ? \&unencode_smtp : '') - || return(0); - $r =~ s/^....//; # maybe something a little better here? - $r = db64($r); - $req->input($r); - return(0) if (!$req->auth_ok); + $r =~ s/^....//; # maybe something a little better here? + $r = db64($r); - do_smtp_gen("", '235', undef, '', - $G::auth_showpt ? "" : '') || return(0); - return(1); + my $callbacks = { user => $u, pass => $p }; + if (exists($G::auth_extras{REALM})) { + $callbacks->{realm} = $G::auth_extras{REALM}; + } + + my $sasl = Authen::SASL->new( + debug => 1, + mechanism => 'DIGEST-MD5', + callback => $callbacks, + ); + my $sasl_client = $sasl->client_new(@digest_uri); + + # Force the DIGEST-MD5 session to use qop=auth. I'm open to exposing this setting + # via some swaks options, but I don't know enough about the protocol to just guess + # here. I do know that letting it auto-negotiate didn't work in my reference + # environment. sendmail advertised auth,auth-int,auth-conf, but when Authen::SASL + # chose auth-int the session would fail (server would say auth succeeded, but then + # immediately terminate my session when I sent MAIL). My reference client + # (Mulberry) always sent auth, and indeed forcing swaks to auth also seems to work. + # If anyone out there knows more about this please let me know. + $sasl_client->property('maxssf' => 0); + + $auth_string = $sasl_client->client_step($r); + if ($e = $sasl_client->error()) { + ptrans('12', "Error received from Authen::SASL sub-system: $e"); + return(0); + } + + do_smtp_gen(eb64($auth_string), '334', \$r, '', + $G::auth_showpt ? "$auth_string" : '', + $G::auth_showpt ? \&unencode_smtp : '') + || return(0); + $r =~ s/^....//; # maybe something a little better here? + $r = db64($r); + + $auth_string = $sasl_client->client_step($r); + if ($e = $sasl_client->error()) { + ptrans('12', "Canceling SASL exchange, error received from Authen::SASL sub-system: $e"); + $auth_string = '*'; + } + #do_smtp_gen(eb64($auth_string), '235', \$r, '', $G::auth_showpt ? "$auth_string" : '') + do_smtp_gen($auth_string, '235', \$r, '', $auth_string) + || return(0); + if ($e = $sasl_client->error()) { + ptrans('12', "Error received from Authen::SASL sub-system: $e"); + return(0); + } + return(0) if (!$sasl_client->is_success()); + + return(1); } # This can handle both CRAM-MD5 and CRAM-SHA1 sub do_smtp_auth_cram { - my $u = shift; # auth user - my $p = shift; # auth password - my $as = shift; # auth string - my $r = ''; # will store smtp response + my $u = shift; # auth user + my $p = shift; # auth password + my $as = shift; # auth string + my $r = ''; # will store smtp response - my $auth_string = "AUTH $as"; - do_smtp_gen($auth_string, '334', \$r, '', '', - $G::auth_showpt ? \&unencode_smtp : '') - || return(0); + my $auth_string = "AUTH $as"; + do_smtp_gen($auth_string, '334', \$r, '', '', $G::auth_showpt ? \&unencode_smtp : '') + || return(0); - $r =~ s/^....//; # maybe something a little better here? - # specify which type of digest we need based on $as - my $d = get_digest($p, $r, ($as =~ /-SHA1$/ ? 'sha1' : 'md5')); - $auth_string = eb64("$u $d"); + $r =~ s/^....//; # maybe something a little better here? + # specify which type of digest we need based on $as + my $d = get_digest($p, $r, ($as =~ /-SHA1$/ ? 'sha1' : 'md5')); + $auth_string = eb64("$u $d"); - do_smtp_gen($auth_string, '235', undef, '', - $G::auth_showpt ? "$u $d" : '') || return(0); - return(1); + do_smtp_gen($auth_string, '235', undef, '', $G::auth_showpt ? "$u $d" : '') || return(0); + return(1); } sub do_smtp_auth_login { - my $u = shift; # auth user - my $p = shift; # auth password - my $as = shift; # auth string - my $z = ''; + my $u = shift; # auth user + my $p = shift; # auth password + my $as = shift; # auth string - my $auth_string = "AUTH $as"; - do_smtp_gen($auth_string, '334', undef, '', '', - $G::auth_showpt ? \&unencode_smtp : '') || return(0); - $auth_string = eb64($u); - $z = $u if ($G::auth_showpt); - do_smtp_gen($auth_string, '334', undef, '', $z, - $G::auth_showpt ? \&unencode_smtp : '') || return(0); - $auth_string = eb64($p); - $z = $p if ($G::auth_showpt); - do_smtp_gen($auth_string, '235', undef, '', $z) || return(0); - return(1); + do_smtp_gen("AUTH $as", '334', undef, '', '', $G::auth_showpt ? \&unencode_smtp : '') + || return(0); + do_smtp_gen(eb64($u), '334', undef, '', $G::auth_showpt ? $u : '', $G::auth_showpt ? \&unencode_smtp : '') + || return(0); + do_smtp_gen(eb64($p), '235', undef, '', + $G::auth_showpt ? ($G::auth_hidepw || $p) : eb64($G::auth_hidepw || $p)) + || return(0); + return(1); } sub do_smtp_auth_plain { - my $u = shift; # auth user - my $p = shift; # auth password - my $as = shift; # auth string + my $u = shift; # auth user + my $p = shift; # auth password + my $as = shift; # auth string - my $auth_string = "AUTH $as " . eb64("\0$u\0$p"); - my $z = ''; - if ($G::auth_showpt) { - $z = "AUTH $as \\0$u\\0$p"; - } - return(do_smtp_gen($auth_string, '235', undef, '', $z)); + return(do_smtp_gen("AUTH $as " . eb64("\0$u\0$p"), '235', undef, '', + $G::auth_showpt ? "AUTH $as \\0$u\\0" . ($G::auth_hidepw || $p) + : "AUTH $as " . eb64("\0$u\0" . ($G::auth_hidepw || $p)))); } sub do_smtp_helo { - my $h = shift; # helo string to use - my $e = shift; # this is a hashref that will be populated w/ server options - my $p = shift; # protocol for the transaction - my $r = ''; # this'll be populated by do_smtp_gen + my $h = shift; # helo string to use + my $e = shift; # this is a hashref that will be populated w/ server options + my $p = shift; # protocol for the transaction + my $r = ''; # this'll be populated by do_smtp_gen - if ($p eq 'esmtp' || $p eq 'lmtp') { - my $l = $p eq 'lmtp' ? "LHLO" : "EHLO"; - if (do_smtp_gen("$l $h", '250', \$r)) { - # $ehlo is designed to hold the advertised options, but I'm not sure how - # to store them all - for instance, SIZE is a simple key/value pair, but - # AUTH lends itself more towards a multilevel hash. What I'm going to do - # is come here and add each key in the way that makes most sense in each - # case. I only need auth for now. - foreach my $l (split(/\n/, $r)) { - $l =~ s/^....//; - if ($l =~ /^AUTH=?(.*)$/) { - map { $e->{AUTH}{uc($_)} = 1 } (split(' ', $1)); - } elsif ($l =~ /^STARTTLS$/) { - $e->{STARTTLS} = 1; - } elsif ($l =~ /^PIPELINING$/) { - $e->{PIPELINING} = 1; - $G::pipeline_adv = 1; - } - } - return(1); - } - } - if ($p eq 'esmtp' || $p eq 'smtp') { - return(do_smtp_gen("HELO $h", '250')); - } + if ($p eq 'esmtp' || $p eq 'lmtp') { + my $l = $p eq 'lmtp' ? "LHLO" : "EHLO"; + if (do_smtp_gen("$l $h", '250', \$r)) { + # There's not a standard structure for the $e hashref, each + # key is stored in the manner that makes the most sense + foreach my $l (split(/\n/, $r)) { + $l =~ s/^....//; + if ($l =~ /^AUTH=?(.*)$/) { + map { $e->{AUTH}{uc($_)} = 1 } (split(' ', $1)); + } elsif ($l =~ /^XCLIENT\s*(.*?)$/) { + $e->{XCLIENT} = {}; # prime the pump in case no attributes were advertised + map { $e->{XCLIENT}{uc($_)} = 1 } (split(' ', $1)); + } elsif ($l =~ /^STARTTLS$/) { + $e->{STARTTLS} = 1; + } elsif ($l =~ /^PIPELINING$/) { + $e->{PIPELINING} = 1; + $G::pipeline_adv = 1; + } elsif ($l =~ /^PRDR$/) { + $e->{PRDR} = 1; + } + } + return(1); + } + } + if ($p eq 'esmtp' || $p eq 'smtp') { + return(do_smtp_gen("HELO $h", '250')); + } - return(0); + return(0); } sub do_smtp_mail { - my $m = shift; # from address + my $e = shift; # ehlo response + my $a = shift; # from address + my $m = "MAIL FROM:<$a>"; - transact(cxn_string => "MAIL FROM:<$m>", expect => '250', defer => 1, - fail_callback => \&smtp_mail_callback); + if ($G::prdr) { + if (!$e->{PRDR}) { + return(1); # PRDR was required but was not advertised. Return error and let caller handle it + } else { + $m .= " PRDR"; + } + } - return(1); # the callback handles failures, so just return here + transact(cxn_string => $m, expect => '250', defer => 1, fail_callback => \&smtp_mail_callback); + + return(0); # the callback handles failures, so just return here } # this only really needs to exist until I figure out a clever way of making # do_smtp_quit the callback while still preserving the exit codes sub smtp_mail_callback { - do_smtp_quit(1, 23); + do_smtp_quit(1, 23); } sub do_smtp_rcpt { - my $m = shift; # string of comma separated recipients - my $f = 0; # The number of failures we've experienced + my $m = shift; # string of comma separated recipients + my $f = 0; # The number of failures we've experienced + my @a = split(/,/, $m); + $G::smtp_rcpt_total = scalar(@a); - my @a = split(/,/, $m); - $G::smtp_rcpt_total = scalar(@a); - foreach my $addr (@a) { - #$f++ if (!do_smtp_gen("RCPT TO:<$addr>", '250')); - transact(cxn_string => "RCPT TO:<$addr>", expect => '250', defer => 1, - fail_callback => \&smtp_rcpt_callback); - } + foreach my $addr (@a) { + transact(cxn_string => 'RCPT TO:<' . $addr . '>', expect => '250', defer => 1, + fail_callback => \&smtp_rcpt_callback); + } - return(1); # the callback handles failures, so just return here - -# # if at least one addr succeeded, we can proceed, else we stop here -# return $f == scalar(@a) ? 0 : 1; + return(1); # the callback handles failures, so just return here } sub smtp_rcpt_callback { - # record that a failure occurred - $G::smtp_rcpt_failures++; + # record that a failure occurred + $G::smtp_rcpt_failures++; - # if the number of failures is the same as the total rcpts (if every rcpt - # rejected), quit. - if ($G::smtp_rcpt_failures == $G::smtp_rcpt_total) { - do_smtp_quit(1, 24); - } + # if the number of failures is the same as the total rcpts (if every rcpt rejected), quit. + if ($G::smtp_rcpt_failures == $G::smtp_rcpt_total) { + do_smtp_quit(1, 24); + } } sub do_smtp_data { - my $m = shift; # string to send - my $b = shift; # be brief in the data we send - my $calls = $G::smtp_rcpt_total - $G::smtp_rcpt_failures; + my $m = shift; # string to send + my $b = shift; # be brief in the data we send + my $r = ''; # will store smtp response + my $e = $G::prdr ? '(250|353)' : '250'; - my $ok = transact(cxn_string => $m, expect => '250', summarize_output => $b); + my $calls = $G::smtp_rcpt_total - $G::smtp_rcpt_failures; + my $ok = transact(cxn_string => $m, expect => $e, summarize_output => $b, return_text => \$r); - # now be a little messy - lmtp is not a lockstep after data - we need to - # listen for as many calls as we had accepted recipients - if ($G::protocol eq 'lmtp') { - foreach my $c (1..($calls-1)) { # -1 because we already got 1 above - $ok += transact(cxn_string => undef, expect => '250'); - } - } - return($ok) + # now be a little messy - lmtp is not a lockstep after data - we need to + # listen for as many calls as we had accepted recipients + if ($G::protocol eq 'lmtp') { + foreach my $c (1..($calls-1)) { # -1 because we already got 1 above + $ok += transact(cxn_string => undef, expect => '250'); + } + } elsif ($G::protocol eq 'esmtp' && $G::prdr && $r =~ /^353 /) { + foreach my $c (1..$calls) { + transact(cxn_string => undef, expect => '250'); # read the status of each recipient off the wire + } + $ok = transact(cxn_string => undef, expect => '250'); # PRDR has an overall acceptance string, read it here and use it as th success indicator + } + return($ok) } sub do_smtp_gen { - my $m = shift; # string to send - my $e = shift; # String we're expecting to get back - my $p = shift; # this is a scalar ref, assign the server return string to it - my $b = shift; # be brief in the data we send - my $x = shift; # if this is populated, print this instead of $m - my $c = shift; # if this is a code ref, call it on the return value b4 print - my $r = ''; # This'll be the return value from transact() - my $time; + my $m = shift; # string to send (if empty, we won't send anything, only read) + my $e = shift; # String we're expecting to get back + my $p = shift; # if this is a scalar ref, assign the server return string to it + my $b = shift; # be brief in the data we print + my $x = shift; # if this is populated, print this instead of $m + my $c = shift; # if this is a code ref, call it on the return value before printing it + my $n = shift; # if true, when the data is sent over the wire, it will not have \r\n appended to it + my $r = shift; # if true, we won't try to ready a response from the server - return transact(cxn_string => $m, expect => $e, return_text => $p, - summarize_output => $b, show_string => $x, - print_callback => $c); + return transact(cxn_string => $m, expect => $e, return_text => $p, + summarize_output => $b, show_string => $x, print_callback => $c, + no_newline => $n, no_read_response => $r, + ); +} + +sub do_smtp_proxy { + my $send = undef; + my $print = undef; + my $no_newline = 0; + + if ($G::proxy{version} == 2) { + $send = pack("W[12]", 0x0D, 0x0A,0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A); + if ($G::proxy{raw}) { + $send .= $G::proxy{raw}; + } else { + # byte 13 + # 4 bits = version (required to be 0x2) + # 4 bits = command (0x2 = LOCAL, 0x1 = PROXY) + $send .= pack("W", 0x20 + ($G::proxy{attr}{command} eq 'LOCAL' ? 0x02 : 0x01)); + if ($G::proxy{attr}{command} eq 'LOCAL') { + # the protocol byte (14, including family and protocol) are ignored with local. Set to zeros + $send .= pack("W", 0x00); + # and, additionally, if we're local, there isn't going to be any address size (bytes 15 and 16) + $send .= pack("W", 0x00); + } else { + # byte 14 + # 4 bits = address family (0x0 = AF_UNSPEC, 0x1 = AF_INET, 0x2 = AF_INET6, 0x3 = AF_UNIX) + # 4 bits = transport protocol (0x0 = UNSPEC, 0x1 = STREAM, 0x2 = DGRAM) + my $byte = 0; + if ($G::proxy{attr}{family} eq 'AF_UNSPEC') { + $byte = 0x00; + } elsif ($G::proxy{attr}{family} eq 'AF_INET') { + $byte = 0x10; + } elsif ($G::proxy{attr}{family} eq 'AF_INET6') { + $byte = 0x20; + } elsif ($G::proxy{attr}{family} eq 'AF_UNIX') { + $byte = 0x30; + } + if ($G::proxy{attr}{protocol} eq 'UNSPEC') { + $byte += 0x0; + } elsif ($G::proxy{attr}{protocol} eq 'STREAM') { + $byte += 0x1; + } elsif ($G::proxy{attr}{protocol} eq 'DGRAM') { + $byte += 0x2; + } + $send .= pack("W", $byte); + + # network portion (bytes 17+) + my $net = pack_ip($G::proxy{attr}{source}) + . pack_ip($G::proxy{attr}{dest}) + . pack("n", $G::proxy{attr}{source_port}) + . pack("n", $G::proxy{attr}{dest_port}); + $send .= pack("n", length($net)) . $net; # add bytes 15+16 (length of network portion) plus the network portion + } + } + + # version 2 is binary, so uuencode it before printing. Also, version 2 REQUIREs that you not send \r\n after it down the wire + $print = eb64($send); + $no_newline = 1; + } else { + if ($G::proxy{raw}) { + $send = "PROXY $G::proxy{raw}"; + } else { + $send = join(' ', 'PROXY', $G::proxy{attr}{family}, $G::proxy{attr}{source}, $G::proxy{attr}{dest}, $G::proxy{attr}{source_port}, $G::proxy{attr}{dest_port}); + } + } + + do_smtp_gen($send, # to be send over the wire + '220', # response code indicating success + undef, # the return string from the server (don't need it) + 0, # do not be brief when printing + $print, # if populated, print this instead of $send + undef, # don't want a post-processing callback + $no_newline, # if true, don't add \r\n to the end of $send when sent over the wire + 1, # don't read a response - we only want to send the value + ); +} + +# no special attempt made at verifying, on purpose +sub pack_ip { + my $ip = shift; + + if ($ip =~ /:/) { + # this is the stupidest piece of code ever. Please tell me all the fun ways it breaks + my @pieces = split(/:/, $ip); + my $p; + shift(@pieces) if ($pieces[0] eq '' && $pieces[1] eq ''); # + foreach my $word (@pieces) { + if ($word eq '') { + foreach my $i (0..(8-scalar(@pieces))) { + $p .= pack("n", 0); + } + } else { + $p .= pack("n", hex($word)); + } + } + return($p); + } else { + return(pack("W*", split(/\./, $ip))); + } } # If we detect that the other side has gone away when we were expecting # to still be reading, come in here to error and die. Abstracted because # the error message will vary depending on the type of connection sub handle_disconnect { - my $e = shift || 6; # this is the code we will exit with - if ($G::link{type} eq 'socket-inet') { - ptrans(12, "Remote host closed connection unexpectedly."); - } elsif ($G::link{type} eq 'socket-unix') { - ptrans(12, "Socket closed connection unexpectedly."); - } elsif ($G::link{type} eq 'pipe') { - ptrans(12, "Child process closed connection unexpectedly."); - } - exit($e); + my $e = shift || 6; # this is the code we will exit with + if ($G::link{type} eq 'socket-inet') { + ptrans(12, "Remote host closed connection unexpectedly."); + } elsif ($G::link{type} eq 'socket-unix') { + ptrans(12, "Socket closed connection unexpectedly."); + } elsif ($G::link{type} eq 'pipe') { + ptrans(12, "Child process closed connection unexpectedly."); + } + exit($e); } sub flush_send_buffer { - my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{wr} : $G::link{sock}; - return if (!$G::send_buffer); - if ($G::link{tls}{active}) { - my $res = Net::SSLeay::write($G::link{tls}{ssl}, $G::send_buffer); - } else { - print $s $G::send_buffer; - } - $G::send_buffer = ''; + my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{wr} : $G::link{sock}; + return if (!$G::send_buffer); + if ($G::link{tls}{active}) { + my $res = Net::SSLeay::write($G::link{tls}{ssl}, $G::send_buffer); + } else { + print $s $G::send_buffer; + } + ptrans(23, hdump($G::send_buffer)) if ($G::show_raw_text); + $G::send_buffer = ''; } sub send_data { - my $d = shift; # data to write - $G::send_buffer .= "$d\r\n"; + my $d = shift; # data to write + my $nnl = shift || 0; # if true, don't add a newline (needed for PROXY v2 support) + $G::send_buffer .= $d . ($nnl ? '' : "\r\n"); } sub recv_line { - # Either an IO::Socket obj or a FH to my child - the thing to read from - my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{re} : $G::link{sock}; - my $r = undef; + # Either an IO::Socket obj or a FH to my child - the thing to read from + my $s = $G::link{type} eq 'pipe' ? $G::link{sock}->{re} : $G::link{sock}; + my $r = undef; + my $t = undef; + my $c = 0; - if ($G::link{tls}{active}) { - $r = Net::SSLeay::read($G::link{tls}{ssl}); - } else { - $r = <$s>; - } - $r =~ s|\r||msg; -#print "in recv_line, returning \$r = $r\n"; - return($r); + while ($G::recv_buffer !~ m|\n|si) { + last if (++$c > 1000); # Maybe I'll remove this once I trust this code more + if ($G::link{tls}{active}) { + $t = Net::SSLeay::read($G::link{tls}{ssl}); + return($t) if (!defined($t)); + + # THIS CODE COPIED FROM THE ELSE BELOW. Found I could trip this condition + # by having the server sever the connection but not have swaks realize the + # connection was gone. For instance, send a PIPELINE mail that includes a + # "-q rcpt". There was a bug in swaks that made it try to send another quit + # later, thus tripping this "1000 reads" error (but only in TLS). + # Short term: add line below to prevent these reads + # Short Term: fix the "double-quit" bug + # Longer term: test to see if remote side closed connection + + # the above line should be good enough but it isn't returning + # undef for some reason. I think heuristically it will be sufficient + # to just look for an empty packet (I hope. gulp). Comment out the + # following line if your swaks seems to be saying that it lost connection + # for no good reason. Then email me about it. + return(undef()) if (!length($t)); + } elsif ($G::link{type} eq 'pipe') { + # XXX in a future release see if I can get read() or equiv to work on a pipe + $t = <$s>; + return($t) if (!defined($t)); + + # THIS CODE COPIED FROM THE ELSE BELOW. + # the above line should be good enough but it isn't returning + # undef for some reason. I think heuristically it will be sufficient + # to just look for an empty packet (I hope. gulp). Comment out the + # following line if your swaks seems to be saying that it lost connection + # for no good reason. Then email me about it. + return(undef()) if (!length($t)); + } else { + # if you're having problems with reads, swap the comments on the + # the following two lines + my $recv_r = recv($s, $t, 8192, 0); + #$t = <$s>; + return($t) if (!defined($t)); + + # the above line should be good enough but it isn't returning + # undef for some reason. I think heuristically it will be sufficient + # to just look for an empty packet (I hope. gulp). Comment out the + # following line if your swaks seems to be saying that it lost connection + # for no good reason. Then email me about it. + return(undef()) if (!length($t)); + + #print "\$t = $t (defined = ", defined($t) ? "yes" : "no", + # "), \$recv_r = $recv_r (", defined($recv_r) ? "yes" : "no", ")\n"; + } + $G::recv_buffer .= $t; + ptrans(33, hdump($t)) if ($G::show_raw_text); + } + + if ($c >= 1000) { + # If you saw this in the wild, I'd love to hear more about it + # at proj-swaks@jetmore.net + ptrans(12, "In recv_line, hit loop counter. Continuing in unknown state"); + } + + # using only bare newlines is bound to cause me problems in the future + # but it matches the expectation we've already been using. All we can + # do is hone in on the proper behavior iteratively. + if ($G::recv_buffer =~ s|^(.*?\n)||si) { + $r = $1; + } else { + ptrans(12, "I'm in an impossible state"); + } + + $r =~ s|\r||msg; + return($r); } # any request which has immediate set will be checking the return code. # any non-immediate request will handle results through fail_callback(). # therefore, only return the state of the last transaction attempted, # which will always be immediate -# We still need to reimplement timing +# defer - if true, does not require immediate flush when pipelining +# cxn_string - What we will be sending the server. If undefined, we won't send, only read +# no_read_response - if true, we won't read a response from the server, we'll just send +# summarize_output - if true, don't print to terminal everything we send to server +# no_newline - if true, do not append \r\n to the data we send to server +# return_text - should be scalar ref. will be assigned reference to what was returned from server +# print_callback - if present and a code reference, will be called with server return data for printing to terminal +# fail_callback - if present and a code reference, will be called on failure sub transact { - my %h = @_; # this is an smtp transaction element - my $ret = 1; # this is our return value - my @handlers = (); # will hold and fail_handlers we need to run - my $time = ''; # used in time lapse calculations + my %h = @_; # this is an smtp transaction element + my $ret = 1; # this is our return value + my @handlers = (); # will hold any fail_handlers we need to run + my $time = ''; # used in time lapse calculations - push(@G::pending_send, \%h); # push onto send queue - if (!($G::pipeline && $G::pipeline_adv) || !$h{defer}) { + push(@G::pending_send, \%h); # push onto send queue + if (!($G::pipeline && $G::pipeline_adv) || !$h{defer}) { - if ($G::show_time_lapse) { - if ($G::show_time_hires) { $time = [Time::HiRes::gettimeofday()]; } - else { $time = time(); } - } + if ($G::show_time_lapse eq 'hires') { + $time = [Time::HiRes::gettimeofday()]; + } + elsif ($G::show_time_lapse eq 'integer') { + $time = time(); + } - while (my $i = shift(@G::pending_send)) { - if ($i->{cxn_string}) { - ptrans(21,$i->{show_string}||$i->{cxn_string},$i->{summarize_output}); - send_data($i->{cxn_string}); - } - push(@G::pending_recv, $i); - } - flush_send_buffer(); - while (my $i = shift(@G::pending_recv)) { - my $buff = ''; - eval { - local $SIG{'ALRM'} = sub { - $buff ="Timeout ($G::link{timeout} secs) waiting for server response"; - die; - }; - alarm($G::link{timeout}); - while ($buff !~ /^\d\d\d /m) { - my $l = recv_line(); - $buff .= $l; - if (!defined($l)) { - $G::link{lost_cxn} = 1; - last; - } - } - chomp($buff); - alarm(0); - }; + while (my $i = shift(@G::pending_send)) { + if (defined($i->{cxn_string})) { + ptrans(21, $i->{show_string} || $i->{cxn_string}, $i->{summarize_output}); + send_data($i->{cxn_string}, $i->{no_newline}); + } + push(@G::pending_recv, $i) if (!$i->{no_read_response}); + } + flush_send_buffer(); - if ($G::show_time_lapse) { - if ($G::show_time_hires) { - $time = sprintf("%0.03f", Time::HiRes::tv_interval($time, - [Time::HiRes::gettimeofday()])); - ptrans(11, "response in ${time}s"); - $time = [Time::HiRes::gettimeofday()]; - } else { - $time = time() - $time; - ptrans(11, "response in ${time}s"); - $time = time(); - } - } + do_smtp_drop() if ($G::drop_before_read); - ${$i->{return_text}} = $buff; - $buff = &{$i->{print_callback}}($buff) - if (ref($i->{print_callback}) eq 'CODE'); - my $ptc; - ($ret,$ptc) = $buff !~ /^$i->{expect} /m ? (0,32) : (1,31); - ptrans($ptc, $buff) if ($buff); - if ($G::link{lost_cxn}) { - if ($G::link{allow_lost_cxn}) { - # this means the calling code wants to handle a lost cxn itself - return($ret); - } else { - # if caller didn't want to handle, we'll handle a lost cxn ourselves - handle_disconnect(); - } - } - if (!$ret && ref($i->{fail_callback}) eq 'CODE') { - push(@handlers, $i->{fail_callback}); - } - } - } - foreach my $h (@handlers) { &{$h}(); } - return($ret); + while (my $i = shift(@G::pending_recv)) { + my $buff = ''; + eval { + local $SIG{'ALRM'} = sub { + $buff = "Timeout ($G::link{timeout} secs) waiting for server response"; + die; + }; + alarm($G::link{timeout}); + while ($buff !~ /^\d\d\d /m) { + my $l = recv_line(); + $buff .= $l; + if (!defined($l)) { + $G::link{lost_cxn} = 1; + last; + } + } + chomp($buff); + alarm(0); + }; + + if ($G::show_time_lapse eq 'hires') { + $time = sprintf("%0.03f", Time::HiRes::tv_interval($time, [Time::HiRes::gettimeofday()])); + ptrans(11, "response in ${time}s"); + $time = [Time::HiRes::gettimeofday()]; + } elsif ($G::show_time_lapse eq 'integer') { + $time = time() - $time; + ptrans(11, "response in ${time}s"); + $time = time(); + } + + ${$i->{return_text}} = $buff; + $buff = &{$i->{print_callback}}($buff) if (ref($i->{print_callback}) eq 'CODE'); + my $ptc; + ($ret,$ptc) = $buff !~ /^$i->{expect} /m ? (0,32) : (1,31); + ptrans($ptc, $buff) if ($buff); + if ($G::link{lost_cxn}) { + if ($G::link{allow_lost_cxn}) { + # this means the calling code wants to handle a lost cxn itself + return($ret); + } else { + # if caller didn't want to handle, we'll handle a lost cxn ourselves + handle_disconnect(); + } + } + if (!$ret && ref($i->{fail_callback}) eq 'CODE') { + push(@handlers, $i->{fail_callback}); + } + } + } + foreach my $h (@handlers) { &{$h}(); } + return($ret); +} + +# a quick-and-dirty hex dumper. Currently used by --show-raw-text +sub hdump { + my $r = shift; + my $c = 0; # counter + my $i = 16; # increment value + my $b; # buffer + + while (length($r) && ($r =~ s|^(.{1,$i})||smi)) { + my $s = $1; # $s will be the ascii string we manipulate for display + my @c = map { ord($_); } (split('', $s)); + $s =~ s|[^\x21-\x7E]|.|g; + + my $hfs = ''; # This is the hex format string for printf + for (my $hc = 0; $hc < $i; $hc++) { + $hfs .= ' ' if (!($hc%4)); + if ($hc < scalar(@c)) { $hfs .= '%02X '; } else { $hfs .= ' '; } + } + + $b .= sprintf("%04d:$hfs %-16s\n", $c, @c, $s); + $c += $i; + } + chomp($b); # inelegant remnant of hdump's previous life + return($b) } sub unencode_smtp { - my $t = shift; + my $t = shift; - my @t = split(' ', $t); - return("$t[0] " . db64($t[1])); + my @t = split(' ', $t, 2); + if ($t[1] =~ /\s/) { + # very occasionally we can have a situation where a successful response will + # be b64 encoded, while an error will not be. Try to tell the difference. + return($t); + } else { + return("$t[0] " . db64($t[1])); + } } -sub process_file { - my $f = shift; - my $h = shift; +sub obtain_from_netrc { + my $field = shift; + my $login = shift; - if (! -e "$f") { - ptrans(12, "File $f does not exist, skipping"); - return; - } elsif (! -f "$f") { - ptrans(12, "File $f is not a file, skipping"); - return; - } elsif (!open(I, "<$f")) { - ptrans(12, "Couldn't open $f, skipping... ($!)"); - return; - } + return if !avail('netrc'); - while () { - chomp; - next if (/^#?\s*$/); # skip blank lines and those that start w/ '#' - my($key,$value) = split(' ', $_, 2); - $h->{uc($key)} = $value; - } - return; + if (my $netrc = Net::Netrc->lookup($G::link{server}, defined($login) ? $login : ())) { + return($netrc->$field); + } + + return; } sub interact { - my($prompt) = shift; - my($regexp) = shift; - my($continue) = shift; - my($response) = ''; + my $prompt = shift; + my $regexp = shift; + my $hide_input = shift; + my $response = ''; - do { - print "$prompt"; - chomp($response = ); - } while ($regexp ne 'SKIP' && $response !~ /$regexp/); + do { + print $prompt; + if (!$hide_input || !$G::protect_prompt || $G::interact_method eq 'default') { + chomp($response = ); + } else { + if ($^O eq 'MSWin32') { + #if ($G::interact_method eq "win32-console" || + # (!$G::interact_method && load("Win32::Console"))) + #{ + # Couldn't get this working in the time I wanted to devote to it + #} + if ($G::interact_method eq "win32-readkey" || + (!$G::interact_method && load("Term::ReadKey"))) + { + $G::interact_method ||= "win32-readkey"; + # the trick to replace input w/ '*' doesn't work on Win32 + # Term::ReadKey, so just use it as an stty replacement + ReadMode('noecho'); + # need to think about this on windows some more + #local $SIG{INT} = sub { ReadMode('restore'); }; + chomp($response = ); + ReadMode('restore'); + } else { + $G::interact_method ||= "default"; + chomp($response = ); + } + } else { + if ($G::interact_method eq "unix-readkey" || (!$G::interact_method && load("Term::ReadKey"))) { + $G::interact_method ||= "unix-readkey"; + my @resp = (); + ReadMode('raw'); + #local $SIG{INT} = + # reevaluate this code - what happens if del is first char we press? + while ((my $kp = ReadKey(0)) ne "\n") { + my $kp_num = ord($kp); + if($kp_num == 127 || $kp_num == 8) { + next if (!scalar(@resp)); + pop(@resp); + print "\b \b"; + } elsif($kp_num >= 32) { + push(@resp, $kp); + print "*"; + } + } + ReadMode('restore'); + $response = join('', @resp); + } elsif ($G::interact_method eq "unix-stty" || (!$G::interact_method && open(STTY, "stty -a |"))) { + $G::interact_method ||= "unix-stty"; + { my $foo = join('', ); } + system('stty', '-echo'); + chomp($response = ); + system('stty', 'echo'); + } else { + $G::interact_method ||= "default"; + chomp($response = ); + } + } + } + } while ($regexp ne 'SKIP' && $response !~ /$regexp/); - return($response); + return($response); +} + +sub get_messageid { + if (!$G::message_id) { + my @time = localtime(); + $G::message_id = sprintf("%04d%02d%02d%02d%02d%02d.%06d\@%s", + $time[5]+1900, $time[4]+1, $time[3], $time[2], $time[1], $time[0], + $$, get_hostname()); + } + + return($G::message_id); } sub get_hostname { - # in some cases hostname returns value but gethostbyname doesn't. - return("") if (!avail("hostname")); - my $h = hostname(); - return("") if (!$h); - my $l = (gethostbyname($h))[0]; - return($l || $h); + # in some cases hostname returns value but gethostbyname doesn't. + return("") if (!avail("hostname")); + + my $h = hostname(); + return("") if (!$h); + + my $l = (gethostbyname($h))[0]; + return($l || $h); } sub get_server { - my $addr = shift; - my $pref = -1; - my $server = "localhost"; + my $addr = shift; + my $pref = -1; + my $server = "localhost"; - if ($addr =~ /\@\[(\d+\.\d+\.\d+\.\d+)\]$/) { - # handle automatic routing of domain literals (user@[1.2.3.4]) - return($1); - } elsif ($addr =~ /\@\#(\d+)$/) { - # handle automatic routing of decimal domain literals (user@#16909060) - $addr = $1; - return(($addr/(2**24))%(2**8) . '.' . ($addr/(2**16))%(2**8) . '.' - .($addr/(2**8))%(2**8) . '.' . ($addr/(2**0))%(2**8)); - } + if ($addr =~ /\@?\[(\d+\.\d+\.\d+\.\d+)\]$/) { + # handle automatic routing of domain literals (user@[1.2.3.4]) + return($1); + } elsif ($addr =~ /\@?\#(\d+)$/) { + # handle automatic routing of decimal domain literals (user@#16909060) + $addr = $1; + return(($addr/(2**24))%(2**8) . '.' . ($addr/(2**16))%(2**8) . '.' . + ($addr/(2**8))%(2**8) . '.' . ($addr/(2**0))%(2**8)); + } + if (!avail("dns")) { + ptrans(12, avail_str("dns"). ". Using $server as mail server"); + return($server); + } + my $res = Net::DNS::Resolver->new(); + $addr =~ s/^.*\@([^\@]*)$/$1/; + return($server) if (!$addr); + $server = $addr; - if (!avail("dns")) { - ptrans(12, avail_str("dns").". Using $server as mail server"); - return($server); - } - my $res = new Net::DNS::Resolver; - - return($server) if ($addr !~ /\@/); - - $addr =~ s/^.*\@([^\@]*)$/$1/; - return($server) if (!$addr); - $server = $addr; - - my @mx = mx($res, $addr); - foreach my $rr (@mx) { - if ($rr->preference < $pref || $pref == -1) { - $pref = $rr->preference; - $server = $rr->exchange; - } - } - return($server); + my @mx = mx($res, $addr); + foreach my $rr (sort { $a->preference <=> $b->preference } @mx) { + if ($G::link{force_ipv4}) { + if ($res->query($rr->exchange, 'A')) { + $server = $rr->exchange; + last; + } + } elsif ($G::link{force_ipv6}) { + if ($res->query($rr->exchange, 'AAAA') || $res->query($rr->exchange, 'A6')) { + $server = $rr->exchange; + last; + } + } else { + # this is the old default behavior. Take the best priority MX, no matter what. + $server = $rr->exchange; + last; + } + } + return($server); } sub load { - my $m = shift; + my $m = shift; - return $G::modules{$m} if (exists($G::modules{$m})); - eval("use $m"); - return $G::modules{$m} = $@ ? 0 : 1; + return $G::modules{$m} if (exists($G::modules{$m})); + eval("use $m"); + return $G::modules{$m} = $@ ? 0 : 1; } # Currently this is just an informational string - it's set on both @@ -977,1052 +1531,2296 @@ sub load { sub avail_str { return $G::dependencies{$_[0]}{errstr}; } sub avail { - my $f = shift; # this is the feature we want to check support for (auth, tls) - my $s = \%G::dependencies; + my $f = shift; # this is the feature we want to check support for (auth, tls) + my $s = \%G::dependencies; - # return immediately if we've already tested this. - return($s->{$f}{avail}) if (exists($s->{$f}{avail})); + # return immediately if we've already tested this. + return($s->{$f}{avail}) if (exists($s->{$f}{avail})); - $s->{$f}{req_failed} = []; - $s->{$f}{opt_failed} = []; - foreach my $m (@{$s->{$f}{req}}) { - push(@{$s->{$f}{req_failed}}, $m) if (!load($m)); - } - foreach my $m (@{$s->{$f}{opt}}) { - push(@{$s->{$f}{opt_failed}}, $m) if (!load($m)); - } + $s->{$f}{req_failed} = []; + $s->{$f}{opt_failed} = []; + foreach my $m (@{$s->{$f}{req}}) { + push(@{$s->{$f}{req_failed}}, $m) if (!load($m)); + } + foreach my $m (@{$s->{$f}{opt}}) { + push(@{$s->{$f}{opt_failed}}, $m) if (!load($m)); + } - if (scalar(@{$s->{$f}{req_failed}})) { - $s->{$f}{errstr} = "$s->{$f}{name} not available: requires " - . join(', ', @{$s->{$f}{req_failed}}); - if (scalar(@{$s->{$f}{opt_failed}})) { - $s->{$f}{errstr} .= ". Also missing optimizing " - . join(', ', @{$s->{$f}{opt_failed}}); - } - return $s->{$f}{avail} = 0; - } else { - if (scalar(@{$s->{$f}{opt_failed}})) { - $s->{$f}{errstr} = "$s->{$f}{name} supported, but missing optimizing " - . join(', ', @{$s->{$f}{opt_failed}}); - } else { - $s->{$f}{errstr} = "$s->{$f}{name} supported"; - } - return $s->{$f}{avail} = 1; - } + if (scalar(@{$s->{$f}{req_failed}})) { + $s->{$f}{errstr} = "$s->{$f}{name} not available: requires " . join(', ', @{$s->{$f}{req_failed}}); + if (scalar(@{$s->{$f}{opt_failed}})) { + $s->{$f}{errstr} .= ". Also missing optimizing " . join(', ', @{$s->{$f}{opt_failed}}); + } + return $s->{$f}{avail} = 0; + } else { + if (scalar(@{$s->{$f}{opt_failed}})) { + $s->{$f}{errstr} = "$s->{$f}{name} supported, but missing optimizing " . + join(', ', @{$s->{$f}{opt_failed}}); + } else { + $s->{$f}{errstr} = "$s->{$f}{name} supported"; + } + return $s->{$f}{avail} = 1; + } } sub get_digest { - my $secr = shift; - my $chal = shift; - my $type = shift || 'md5'; - my $ipad = chr(0x36) x 64; - my $opad = chr(0x5c) x 64; + my $secr = shift; + my $chal = shift; + my $type = shift || 'md5'; + my $ipad = chr(0x36) x 64; + my $opad = chr(0x5c) x 64; - if ($chal !~ /^ 64) { - if ($type eq 'md5') { - $secr = Digest::MD5::md5($secr); - } elsif ($type eq 'sha1') { - $secr = Digest::SHA1::sha1($secr); - } - } else { - $secr .= chr(0) x (64 - length($secr)); - } + if (length($secr) > 64) { + if ($type eq 'md5') { + $secr = Digest::MD5::md5($secr); + } elsif ($type eq 'sha1') { + $secr = Digest::SHA::sha1($secr); + } + } else { + $secr .= chr(0) x (64 - length($secr)); + } - my $digest = $type eq 'md5' - ? Digest::MD5::md5_hex(($secr ^ $opad), - Digest::MD5::md5(($secr ^ $ipad), $chal)) - : Digest::SHA1::sha1_hex(($secr ^ $opad), - Digest::SHA1::sha1(($secr ^ $ipad), $chal)); - return($digest); + my $digest = $type eq 'md5' ? Digest::MD5::md5_hex(($secr ^ $opad), Digest::MD5::md5(($secr ^ $ipad), $chal)) + : Digest::SHA::sha1_hex(($secr ^ $opad), Digest::SHA::sha1(($secr ^ $ipad), $chal)); + return($digest); } sub test_support { - my $s = \%G::dependencies; + my $return = shift; + my $lines = []; + my $s = \%G::dependencies; - foreach my $act (sort { $s->{$a}{name} cmp $s->{$b}{name} } keys %$s) { - ptrans(avail($act) ? 11 : 12, avail_str($act)); - #if (avail($act)) { - # #ptrans(11, "$s->{$act}{name} supported"); - # ptrans(11, avail_err($act)); - #} else { - # ptrans(12, avail_err($act)); - #} - } + foreach my $act (sort { $s->{$a}{name} cmp $s->{$b}{name} } keys %$s) { + if ($return) { + push(@$lines, @{ptrans(avail($act) ? 11 : 12, avail_str($act), undef, 1)}); + } + else { + ptrans(avail($act) ? 11 : 12, avail_str($act)); + } + } + + if ($return) { + return($lines); + } } sub time_to_seconds { - my $t = shift || 30; + my $t = shift; - if ($t !~ /^(\d+)([hms])?/i) { - return(30); # error condition - just use default value - } else { - my $r = $1; - my $u = lc($2); - if ($u eq 'h') { - return($r * 3600); - } elsif ($u eq 'm') { - return($r * 60); - } else { - return($r); - } - } + if ($t !~ /^(\d+)([hms])?$/i) { + ptrans(12, 'Unknown timeout format \'' . $t . '\''); + exit(1); + } else { + my $r = $1; + my $u = lc($2); + if ($u eq 'h') { + return($r * 3600); + } elsif ($u eq 'm') { + return($r * 60); + } else { + return($r); + } + } +} + +sub load_dependencies { + %G::dependencies = ( + auth => { name => "Basic AUTH", opt => ['MIME::Base64'], + req => [] }, + auth_cram_md5 => { name => "AUTH CRAM-MD5", req => ['Digest::MD5'] }, + auth_cram_sha1 => { name => "AUTH CRAM-SHA1", req => ['Digest::SHA'] }, + auth_ntlm => { name => "AUTH NTLM", req => ['Authen::NTLM'] }, + auth_digest_md5 => { name => "AUTH DIGEST-MD5", req => ['Authen::SASL'] }, + dns => { name => "MX Routing", req => ['Net::DNS'] }, + netrc => { name => 'Netrc Credentials', req => ['Net::Netrc'] }, + tls => { name => "TLS", req => ['Net::SSLeay'] }, + pipe => { name => "Pipe Transport", req => ['IPC::Open2'] }, + socket => { name => "Socket Transport", req => ['IO::Socket'] }, + ipv6 => { name => "IPv6", req => ['IO::Socket::INET6'] }, + date_manip => { name => "Date Manipulation", req => ['POSIX'] }, + hostname => { name => "Local Hostname Detection", req => ['Sys::Hostname'] }, + hires_timing => { name => "High Resolution Timing", req => ['Time::HiRes'] }, + ); +} + +sub process_opt_silent { + my $opt = shift; + my $arg = shift; + + if ($arg =~ /^[123]$/) { + return($arg); + } + else { + return(1); + } +} + +sub get_option_struct { + use constant { + OP_ARG_OPT => 0x01, # option takes an optional argument + OP_ARG_REQ => 0x02, # option takes a required argument + OP_ARG_NONE => 0x04, # option does not take any argument (will return boolean) + OP_FROM_PROMPT => 0x08, # option prompts for an argument if none provided + OP_FROM_FILE => 0x10, # option treats arg of '-' to mean 'read from stdin' (no prompt) + OP_DEPRECATED => 0x20, # This option is deprecated + }; + + @G::raw_option_data = ( + # location of config file. Note that the "config" option is processed differently + # than any other option because it needs to be processed before standard option processing + # can happen. We still define it here to make Getopt::Long and fetch_args() happy. + { opts => ['config'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'config_file', type => 'scalar', }, + # envelope-(f)rom address + { opts => ['from', 'f'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'From: ', match => '^.*$', + okey => 'mail_from', type => 'scalar', }, + # envelope-(t)o address + { opts => ['to', 't'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'To: ', match => '^.+$', + okey => 'mail_to', type => 'scalar', }, + # (h)elo string + { opts => ['helo', 'ehlo', 'lhlo', 'h'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Helo: ', match => '^.*$', + okey => 'mail_helo', type => 'scalar', }, + # (s)erver to use + { opts => ['server', 's'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Server: ', match => '^.*$', + okey => 'mail_server', type => 'scalar', }, + # force ipv4 only + { opts => ['4'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'force_ipv4', type => 'scalar', }, + # force ipv6 only + { opts => ['6'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'force_ipv6', type => 'scalar', }, + # copy MX/routing from another domain + { opts => ['copy-routing'], suffix => ':s', + cfgs => OP_ARG_REQ, + okey => 'copy_routing', type => 'scalar', }, + # (p)ort to use + { opts => ['port', 'p'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Port: ', match => '^\w+$', + okey => 'mail_port', type => 'scalar', }, + # protocol to use (smtp, esmtp, lmtp) + { opts => ['protocol'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'mail_protocol', type => 'scalar', }, + # (d)ata portion ('\n' for newlines) + { opts => ['data', 'd'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT|OP_FROM_FILE, + prompt => 'Data: ', match => '^.*$', + okey => 'mail_data', type => 'scalar', }, + # use the --dump text as default body + { opts => ['dump-as-body', 'dab'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'dump_as_body', type => 'scalar', }, + # implies --dump-as-body; forces raw passwords to be used + { opts => ['dump-as-body-shows-password', 'dabsp'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'dab_sp', type => 'scalar', }, + # timeout for each trans (def 30s) + { opts => ['timeout'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Timeout: ', match => '^\d+[hHmMsS]?$', + okey => 'timeout', type => 'scalar', }, + # (q)uit after + { opts => ['quit-after', 'quit', 'q'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'quit_after', type => 'scalar', }, + # drop after (don't quit, just drop) + { opts => ['drop-after', 'drop', 'da'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'drop_after', type => 'scalar', }, + # drop after send (between send and read) + { opts => ['drop-after-send', 'das'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'drop_after_send', type => 'scalar', }, + # do (n)ot print data portion + { opts => ['suppress-data', 'n'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'suppress_data', type => 'scalar', }, + # force auth, exit if not supported + { opts => ['auth', 'a'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'auth', type => 'scalar', }, + # user for auth + { opts => ['auth-user', 'au'], suffix => ':s', + cfgs => OP_ARG_OPT, # we dynamically change this later + okey => 'auth_user', type => 'scalar', }, + # pass for auth + { opts => ['auth-password', 'ap'], suffix => ':s', + cfgs => OP_ARG_OPT, # we dynamically change this later + okey => 'auth_pass', type => 'scalar', }, + # auth type map + { opts => ['auth-map', 'am'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'auth_map', type => 'scalar', }, + # extra, authenticator-specific options + { opts => ['auth-extra', 'ae'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'auth_extra', type => 'scalar', }, + # hide passwords when possible + { opts => ['auth-hide-password', 'ahp'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'auth_hidepw', type => 'scalar', }, + # translate base64 strings + { opts => ['auth-plaintext', 'apt'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'auth_showpt', type => 'scalar', }, + # auth optional (ignore failure) + { opts => ['auth-optional', 'ao'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'auth_optional', type => 'scalar', }, + # req auth if avail + { opts => ['auth-optional-strict', 'aos'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'auth_optional_strict', type => 'scalar', }, + # report capabilties + { opts => ['support'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'get_support', type => 'scalar', }, + # local interface to use + { opts => ['local-interface', 'li'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Interface: ', match => '^.*$', + okey => 'lint', type => 'scalar', }, + # local port + { opts => ['local-port', 'lport', 'lp'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Local Port: ', match => '^\w+$', + okey => 'lport', type => 'scalar', }, + # use TLS + { opts => ['tls'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'tls', type => 'scalar', }, + # use tls if available + { opts => ['tls-optional', 'tlso'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'tls_optional', type => 'scalar', }, + # req tls if avail + { opts => ['tls-optional-strict', 'tlsos'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'tls_optional_strict', type => 'scalar', }, + # use tls if available + { opts => ['tls-on-connect', 'tlsc'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'tls_on_connect', type => 'scalar', }, + # local cert to present to server + { opts => ['tls-cert'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'tls_cert', type => 'scalar', }, + # local key to present to server + { opts => ['tls-key'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'tls_key', type => 'scalar', }, + # tls protocol to use + { opts => ['tls-protocol', 'tlsp'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'tls_protocol', type => 'scalar', }, + # tls cipher to use + { opts => ['tls-cipher'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'tls_cipher', type => 'scalar', }, + # save tls peer certificate + { opts => ['tls-get-peer-cert'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'tls_get_peer_cert', type => 'scalar', }, + # hostname to request in TLS SNI header + { opts => ['tls-sni'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'tls_sni_hostname', type => 'scalar', }, + # require verification of server certificate + { opts => ['tls-verify'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'tls_verify', type => 'scalar', }, + # local key to present to server + { opts => ['tls-ca-path'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'tls_ca_path', type => 'scalar', }, + # suppress output to varying degrees + { opts => ['silent', 'S'], suffix => ':i', + cfgs => OP_ARG_OPT, + callout => \&process_opt_silent, + okey => 'silent', type => 'scalar', }, + # Don't strip From_ line from DATA + { opts => ['no-strip-from', 'nsf'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'no_strip_from', type => 'scalar', }, + # Don't show send/receive hints (legacy) + { opts => ['no-hints', 'nth'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'no_hints', type => 'scalar', }, + # Don't show transaction hints + { opts => ['no-send-hints', 'nsh'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'no_hints_send', type => 'scalar', }, + # Don't show transaction hints + { opts => ['no-receive-hints', 'nrh'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'no_hints_recv', type => 'scalar', }, + # Don't show transaction hints + { opts => ['no-info-hints', 'nih'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'no_hints_info', type => 'scalar', }, + # Don't show reception lines + { opts => ['hide-receive', 'hr'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'hide_receive', type => 'scalar', }, + # Don't show sending lines + { opts => ['hide-send', 'hs'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'hide_send', type => 'scalar', }, + # Don't echo input on potentially sensitive prompts + { opts => ['protect-prompt', 'pp'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'protect_prompt', type => 'scalar', }, + # Don't show any swaks-generated, non-error informational lines + { opts => ['hide-informational', 'hi'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'hide_informational', type => 'scalar', }, + # Don't send any output to the terminal + { opts => ['hide-all', 'ha'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'hide_all', type => 'scalar', }, + # print lapse for send/recv + { opts => ['show-time-lapse', 'stl'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'show_time_lapse', type => 'scalar', }, + # print version and exit + { opts => ['version'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'version', type => 'scalar', }, + # print help and exit + { opts => ['help'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'help', type => 'scalar', }, + # don't touch the data + { opts => ['no-data-fixup', 'ndf'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'no_data_fixup', type => 'scalar', }, + # show dumps of the raw read/written text + { opts => ['show-raw-text', 'raw'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'show_raw_text', type => 'scalar', }, + # specify file to write to + { opts => ['output', 'output-file'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'output_file', type => 'scalar', }, + # specify file to write to + { opts => ['output-file-stdout'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'output_file_stdout', type => 'scalar', }, + # specify file to write to + { opts => ['output-file-stderr'], suffix => '=s', + cfgs => OP_ARG_REQ, + okey => 'output_file_stderr', type => 'scalar', }, + # command to communicate with + { opts => ['pipe'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Pipe: ', match => '^.+$', + okey => 'pipe_cmd', type => 'scalar', }, + # unix domain socket to talk to + { opts => ['socket'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'Socket File: ', match => '^.+$', + okey => 'socket', type => 'scalar', }, + # the content of the body of the DATA + { opts => ['body'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT|OP_FROM_FILE, + prompt => 'Body: ', match => '.+', + okey => 'body_822', type => 'scalar', }, + # A file to attach + { opts => ['attach-name'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'attach_name', akey => 'attach_accum', type => 'list', }, + # A file to attach + { opts => ['attach-type'], suffix => ':s', + cfgs => OP_ARG_REQ, + okey => 'attach_type', akey => 'attach_accum', type => 'list', }, + # A file to attach + { opts => ['attach'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_FILE, + okey => 'attach_attach', akey => 'attach_accum', type => 'list', }, + # A file to attach + { opts => ['attach-body'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_FILE, + okey => 'attach_body', akey => 'attach_accum', type => 'list', }, + # replacement for %NEW_HEADERS% DATA token + { opts => ['add-header', 'ah'], suffix => ':s', + cfgs => OP_ARG_REQ, + okey => 'add_header', type => 'list', }, + # replace header if exist, else add + { opts => ['header'], suffix => ':s', + cfgs => OP_ARG_REQ, + okey => 'header', type => 'list', }, + # build options and dump + { opts => ['dump'], suffix => ':s', + cfgs => OP_ARG_OPT, + okey => 'dump_args', type => 'scalar', }, + # build options and dump the generate message body (EML) + { opts => ['dump-mail'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'dump_mail', type => 'scalar', }, + # attempt PIPELINING + { opts => ['pipeline'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'pipeline', type => 'scalar', }, + # attempt PRDR + { opts => ['prdr'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'prdr', type => 'scalar', }, + # use getpwuid building -f + { opts => ['force-getpwuid'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'force_getpwuid', type => 'scalar', }, + + # XCLIENT + # These xclient_attrs options all get pushed onto an array so that we can determine their order later + # argument is a raw XCLIENT string + { opts => ['xclient'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT string: ', match => '^.+$', + okey => 'xclient_raw', akey => 'xclient_accum', type => 'list', }, + # XCLIENT NAME + { opts => ['xclient-name'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT name: ', match => '^.+$', + okey => 'xclient_name', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT ADDR + { opts => ['xclient-addr'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT addr: ', match => '^.+$', + okey => 'xclient_addr', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT PORT + { opts => ['xclient-port'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT port: ', match => '^.+$', + okey => 'xclient_port', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT PROTO + { opts => ['xclient-proto'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT proto: ', match => '^.+$', + okey => 'xclient_proto', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT DESTADDR + { opts => ['xclient-destaddr'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT destaddr: ', match => '^.+$', + okey => 'xclient_destaddr', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT DESTPORT + { opts => ['xclient-destport'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT destport: ', match => '^.+$', + okey => 'xclient_destport', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT HELO + { opts => ['xclient-helo'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT helo: ', match => '^.+$', + okey => 'xclient_helo', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT LOGIN + { opts => ['xclient-login'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT login: ', match => '^.+$', + okey => 'xclient_login', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT REVERSE_NAME + { opts => ['xclient-reverse-name'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'XCLIENT reverse_name: ', match => '^.+$', + okey => 'xclient_reverse_name', akey => 'xclient_accum', type => 'scalar', }, + # XCLIENT delimiter. Used to indicate that user wants to start a new xclient attr grouping + { opts => ['xclient-delim'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'xclient_delim', akey => 'xclient_accum', type => 'list', }, + # if set, XCLIENT will proceed even if XCLIENT not advertised + { opts => ['xclient-optional'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'xclient_optional', type => 'scalar', }, + # proceed if xclient not offered, but fail if offered and not accepted + { opts => ['xclient-optional-strict'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'xclient_optional_strict', type => 'scalar', }, + # we send xclient after starttls by default. if --xclient-before-starttls will send before tls + { opts => ['xclient-before-starttls'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'xclient_before_starttls', type => 'scalar', }, + # Don't require that the --xclient-ATTR attributes be advertised by server + { opts => ['xclient-no-verify'], suffix => '', + cfgs => OP_ARG_NONE, + okey => 'xclient_no_verify', type => 'scalar', }, + ## xclient send by default after first helo, but can be sent almost anywhere (cf quit-after) + # { opts => ['xclient-after'], suffix => ':s', + # okey => 'xclient_after', type => 'scalar', }, + + # PROXY + # argument is the raw PROXY string + { opts => ['proxy'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY string: ', match => '^.+$', + okey => 'proxy_raw', type => 'scalar', }, + # PROXY version (1 or 2) + { opts => ['proxy-version'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY version: ', match => '^[12]$', + okey => 'proxy_version', type => 'scalar', }, + # PROXY protocol family (TCP4 or TCP6) + { opts => ['proxy-family'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY family: ', match => '^.+$', + okey => 'proxy_family', type => 'scalar', }, + # PROXY protocol command (LOCAL or PROXY) + { opts => ['proxy-command'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY command: ', match => '^.+$', + okey => 'proxy_command', type => 'scalar', }, + # PROXY transport protocol + { opts => ['proxy-protocol'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY protocol: ', match => '^.+$', + okey => 'proxy_protocol', type => 'scalar', }, + # PROXY source address (IPv4 or IPv6) + { opts => ['proxy-source'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY source: ', match => '^.+$', + okey => 'proxy_source', type => 'scalar', }, + # PROXY source port + { opts => ['proxy-source-port'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY source_port: ', match => '^.+$', + okey => 'proxy_source_port', type => 'scalar', }, + # PROXY destination address (IPv4 or IPv6) + { opts => ['proxy-dest'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY dest: ', match => '^.+$', + okey => 'proxy_dest', type => 'scalar', }, + # PROXY destination port + { opts => ['proxy-dest-port'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_FROM_PROMPT, + prompt => 'PROXY dest_port: ', match => '^.+$', + okey => 'proxy_dest_port', type => 'scalar', }, + + # this option serve no purpose other than testing the deprecation system + { opts => ['trigger-deprecation'], suffix => ':s', + cfgs => OP_ARG_REQ|OP_DEPRECATED, + okey => 'trigger_deprecation', type => 'scalar', }, + ); + + return(\@G::raw_option_data); +} + +# returns %O, the large raw option hash +# This sub is a jumping point. We will construct an argv based on the different ways that options can be specified +# and call GetOptions multiple times. We are essentially "layering" options. First we load from a config file (if +# exists/specified), then from any environment variables, then the actual command line. +sub load_args { + my %ARGS = (); # this is the structure that gets returned + my @fakeARGV = (); + + # we load our options processing hash here. We abstract it back from the + # native getopt-format because we need to be able to intercept "no-" options + my $option_list = get_option_struct(); + + # do a loop through the options and make sure they are structured the way we expect + foreach my $e (@$option_list) { + if (!exists($e->{okey}) || !$e->{okey}) { + ptrans(12, 'Option configuration missing an okey (this is a swaks bug)'); + exit(1); + } + elsif (!exists($e->{opts}) || ref($e->{opts}) ne 'ARRAY') { + ptrans(12, 'Option ' . $e->{okey} . ' missing or corrupt opts key (this is a swaks bug)'); + exit(1); + } + elsif (!exists($e->{suffix})) { + ptrans(12, 'Option ' . $e->{okey} . ' missing suffix key (this is a swaks bug)'); + exit(1); + } + elsif (!exists($e->{type}) || $e->{type} !~ /^(scalar|list)$/) { + ptrans(12, 'Option ' . $e->{okey} . ' missing or invalid type key (this is a swaks bug)'); + exit(1); + } + elsif (!exists($e->{cfgs})) { + ptrans(12, 'Option ' . $e->{okey} . ' missing cfgs key (this is a swaks bug)'); + exit(1); + } + + $e->{akey} = $e->{okey} if (!exists($e->{akey})); + + # 'cfgs' stores the okey config for easier access later + $ARGS{cfgs}{$e->{okey}} = $e; + } + + # we want to process config files first. There's a default config file in + # ~/.swaksrc, but it is possible for the user to override this with the + # --config options. So, find the one and only file we will use here. + # If we encounter --config in later processing it is a noop. + # first find the default file + my $config_file = ''; + my $skip_config = 0; + my $config_is_default = 1; + foreach my $v (qw(SWAKS_HOME HOME LOGDIR)) { + if (exists($ENV{$v}) && length($ENV{$v}) && -f $ENV{$v} . '/.swaksrc') { + $config_file = $ENV{$v} . '/.swaksrc'; + last; + } + } + # then look through the ENV args to see if another file set there + if (exists($ENV{SWAKS_OPT_config})) { + if (!$ENV{SWAKS_OPT_config}) { + # if exist but not set, it just means "don't use default file" + $skip_config = 1; + } else { + $config_file = $ENV{SWAKS_OPT_config}; + $config_is_default = 0; + } + } + # lastly go (backwards) through original command line looking for config file, + # choosing the first one found (meaning last one specified) + for (my $i = scalar(@ARGV) - 1; $i >= 0; $i--) { + if ($ARGV[$i] =~ /^-?-config$/) { + if ($i == scalar(@ARGV) - 1 || $ARGV[$i+1] =~ /^-/) { + $skip_config = 1; + } else { + $config_file = $ARGV[$i+1]; + $config_is_default = 0; + $skip_config = 0; + } + last; + } + } + + # All of the above will result in $config_file either being empty or + # containing the one and only config file we will use (though merged with DATA) + if (!$skip_config) { + my @configs = ('&DATA'); + push(@configs, $config_file) if ($config_file); + foreach my $configf (@configs) { + my @fakeARGV = (); + if (open(C, '<' . $configf)) { + # "#" in col 0 is a comment + while (defined(my $m = )) { + next if ($m =~ m|^#|); + chomp($m); + $m = '--' . $m if ($m !~ /^-/); + push(@fakeARGV, split(/\s/, $m, 2)); + } + close(C); + } elsif (!$config_is_default && $configf eq $config_file) { + # we only print an error if the config was specified explicitly + ptrans(12, 'Config file ' . $configf . ' could not be opened ($!). Exiting'); + exit(1); + } + + # OK, all that work to load @fakeARGV with values from the config file. Now + # we just need to process it. (don't call if nothing set in @fakeARGV) + fetch_args(\%ARGS, $option_list, \@fakeARGV) if (scalar(@fakeARGV)); + check_opt_processing(\@fakeARGV, 'Config file ' . $configf); + } + } + + # OK, %ARGS contains all the settings from the config file. Now do it again + # with SWAKS_OPT_* environment variables + @fakeARGV = (); + foreach my $v (sort keys %ENV) { + if ($v =~ m|^SWAKS_OPT_(.*)$|) { + my $tv = $1; $tv =~ s|_|-|g; + push(@fakeARGV, '--' . $tv); + push(@fakeARGV, $ENV{$v}) if (length($ENV{$v})); + } + } + fetch_args(\%ARGS, $option_list, \@fakeARGV) if (scalar(@fakeARGV)); + check_opt_processing(\@fakeARGV, 'environment'); + + # and now, after all of that, process the actual cmdline args + fetch_args(\%ARGS, $option_list, \@ARGV) if (scalar(@ARGV)); + check_opt_processing(\@ARGV, 'command line'); + + return(\%ARGS); +} + +# if there's anything left in the fake argv after Getopts processed it, it's an error. There's nothing +# that can be passed in to swaks that isn't an option or an argument to an option, all of which Getopt +# should consume. So if there's anything left, the user did something weird. Just let them know and +# error instead of letting them think their ignored stuff is working. +sub check_opt_processing { + my $argv_local = shift; + my $option_type = shift; + + if (scalar(@$argv_local)) { + ptrans(12, 'Data left in option list when processing ' . $option_type . ' (' . + join(', ', map { "'$_'" } (@$argv_local)) . + '). Exiting'); + exit(1); + } +} + +sub fetch_args { + my $r = shift; + my $l = shift; + my $argv_local = shift; + + my %to_delete = (); + + # need to rewrite header-HEADER opts before std option parsing + # also see if there are any --no- options that need to be processed + RUNOPTS: + for (my $i = 0; $i < scalar(@$argv_local); $i++) { + # before doing any option processing, massage from the optional '--option=arg' format into '--option arg' format. + if ($argv_local->[$i] =~ /^(-[^=]+)=(.*)$/) { + $argv_local->[$i] = $1; + splice(@$argv_local, $i+1, 0, $2); + } + + # -g is not really necessary. It is now deprecated. During the deprecation window, make + # it a straight-up alias to `--data -`. If has already appeared, just ignore -g. If + # --data has not appeared, change -g into `--data -`. + if ($argv_local->[$i] =~ /^-?-g$/) { + deprecate('The -g option is deprecated and will be removed. Please use \'--data -\' instead.'); + if (scalar(grep(/^-?-data$/, @$argv_local)) || check_arg('mail_data', $r)) { + # if --data appears in the current stream or has already appeared in a previous stream, ignore -g + splice(@$argv_local, $i, 1); # remove the current index from @$argv_local + redo(RUNOPTS); # since there's now a new value at $i, redo this iteration of the loop + } + else { + # if we haven't seen --data yet, change -g into `--data -` + splice(@$argv_local, $i, 1, '--data', '-'); + } + } + + if ($argv_local->[$i] =~ /^-?-h(?:eader)?-([^:]+):?$/) { + # rewrite '--header-Foo bar' into '--header "Foo: bar"' + $argv_local->[$i] = "--header"; + $argv_local->[$i+1] = $1 . ': ' . $argv_local->[$i+1]; + } + elsif ($argv_local->[$i] =~ /^-?-no-h(?:eader)?-/) { + # rewrite '--no-header-Foo' into '--no-header' + $argv_local->[$i] = "--no-header"; + } + } + + # build the actual hash we will pass to GetOptions from our config list ($l): + # In the end I decided to build this at each call of this sub so that $r + # is defined. It's not much of a performance issue. + my %options = (); + foreach my $e (@$l) { + my $k = join('|', @{$e->{opts}}) . $e->{suffix}; + my $nk = join('|', map { "no-$_" } (@{$e->{opts}})); + my $eval; + if ($e->{type} eq 'scalar' || $e->{type} eq 'list') { + $eval = "\$options{\$k} = sub { store_option(\$e, \$r, 0, \@_); };" + . "\$options{\$nk} = sub { store_option(\$e, \$r, 1, \@_); };"; + } + else { + ptrans(12, "Unknown option type '$e->{type}' (this is a swaks bug)"); + exit(1); + } + eval($eval); + if ($@) { + chomp($@); + ptrans(12, "Unable to load callback for $k option processing: $@"); + exit(1); + } + } + + if (!load("Getopt::Long")) { + ptrans(12, "Unable to load Getopt::Long for option processing, Exiting"); + exit(1); + } + + Getopt::Long::Configure("no_ignore_case"); + Getopt::Long::GetOptionsFromArray($argv_local, %options) || exit(1); +} + +sub store_option { + my $cfg_struct = shift; # this is the option definition structure + my $opt_struct = shift; # this is where we will be saving the option for later retrieval + my $remove = shift; # if true, we received a "no-" version of the option, remove all previous instances + my $opt_name = shift; # --xclient-name || --dump-mail || -f + my $opt_value = shift; # NAME || undef || foo@example.com + my $accum_key = $cfg_struct->{akey}; # xclient_attrs || dump_mail || mail_from + my $opt_key = $cfg_struct->{okey}; # xclient_name || dump_mail || mail_from + my $type = $cfg_struct->{type}; # scalar or list + + # print "store_options called -> $cfg_struct, $opt_struct, $opt_name, $opt_value, $accum_key, $opt_key, $type\n"; + + if ($cfg_struct->{cfgs} & OP_DEPRECATED) { + deprecate("Option --$opt_name will be removed in the future. Please see documentation for more information."); + } + + # 'accum' stores lists of the order they were received in + $opt_struct->{accums}{$accum_key} ||= []; + # 'values' stores the actual values and the name of the option that was used to pass it + $opt_struct->{values}{$opt_key} ||= []; + + # if we're recording a scalar or were asked to remove, reset the values list to throw away any previous values + # and remove any previous recordings of this okey from the accumulator list + if ($type eq 'scalar' || $remove) { + $opt_struct->{values}{$opt_key} = []; + $opt_struct->{accums}{$accum_key} = [ grep { $_ ne $opt_key } (@{$opt_struct->{accums}{$accum_key}}) ]; + } + + # if we were asked to remove (which means called with a "--no-" prefix), get out now, there's nothing to record + return if ($remove); + + push(@{$opt_struct->{accums}{$accum_key}}, $opt_key); + + my $arg = $opt_value; + if ($cfg_struct->{callout}) { + $arg = $cfg_struct->{callout}("$opt_name", $arg); + } + + push(@{$opt_struct->{values}{$opt_key}}, { + okey => $opt_key, + akey => $accum_key, + opt => "$opt_name", + arg => $arg, + }); +} + +# take a string and quote it such that it could be used in the shell +# O'Reilley -> 'O'\''Reilley' +sub shquote { my $s = shift; $s =~ s%'%'\\''%g; return "'$s'"; } + +sub reconstruct_options { + my $o = shift; # ref to raw option hash (as returned by load_args) + my @c = (); # array to hold our reconstructed command line + my %already_seen = (); # for okeys like xclient_attrs, they only need to be processed once + my %indexer = (); + + foreach my $opt (@G::raw_option_data) { + next if ($already_seen{$opt->{akey}}); + next if (!exists($o->{accums}{$opt->{akey}})); + + foreach my $okey (@{$o->{accums}{$opt->{akey}}}) { + $indexer{$okey} ||= 0; + my $optStruct = $o->{values}{$okey}[$indexer{$okey}]; + my $lopt = $o->{cfgs}{$okey}{opts}[0]; + + push(@c, '--'.$lopt); + if (length($optStruct->{arg}) && !($o->{cfgs}{$okey}{cfgs} & OP_ARG_NONE)) { + if ($okey eq 'auth_pass') { + push(@c, shquote('%RAW_PASSWORD_STRING%')); + } + else { + push(@c, shquote($optStruct->{arg})); + } + } + } + $already_seen{$opt->{akey}} = 1; + } + + #print join(', ', @c), "\n"; + return join(' ', @c); +} + +sub get_accum { + my $accum_key = shift; + my $userOpts = shift; + + if (!exists($userOpts->{accums}{$accum_key})) { + return([]); + } + + return($userOpts->{accums}{$accum_key}); +} + +# I might change this interface later, but I want a way to check whether the user provided the option +# without actually processing it. +sub check_arg { + my $opt = shift; + my $userOpts = shift; + + if (exists($userOpts->{values}{$opt}) && scalar(@{$userOpts->{values}{$opt}})) { + return(1); + } + + return(0); +} + +# get the next value for $opt without doing any processing or popping it off of the list. +sub peek_arg { + my $opt = shift; # this should correspond to an okey from the @G::raw_option_data array + my $userOpts = shift; # all options we got from the command line + + if (!exists($userOpts->{values}{$opt})) { + return(undef()); + } + + if (!scalar(@{$userOpts->{values}{$opt}})) { + return(undef()); + } + + return($userOpts->{values}{$opt}[0]{arg}); +} + +# there was a ton of repeated, boiler plate code in process_args. Attempt to abstract it out to get_arg +sub get_arg { + my $opt = shift; # this should correspond to an okey from the @G::raw_option_data array + my $userOpts = shift; # all options we got from the command line + my $optConfig = shift; + my $force = shift; + my $arg; + my $argExt; + my $return; + + # print "in get_arg, opt = $opt\n"; + + # If the user didn't pass in a specific option config, look it up from the global option config + if (!$optConfig) { + if (!exists($userOpts->{cfgs}{$opt})) { + ptrans(12, "Internal option processing error: asked to evaluate non-existent option $opt"); + exit(1); + } + $optConfig = $userOpts->{cfgs}{$opt}; + } + + # $arg will be the value actually provided on the command line + # !defined = not provided + # defined && !length = option provided but no argument provided + # defined && length = option provided and argument provided + if (!exists($userOpts->{values}{$opt})) { + # if the caller passed in $force, we act as if the option is present with an empty arg + # this is used when we need to use get_arg features like interact() even when the user + # didn't specify the option (specifically, --auth forces --auth-password to need to be + # processed, even if the user didn't pass it in) + $arg = $force ? '' : undef(); + } + else { + $argExt = shift(@{$userOpts->{values}{$opt}}); + $arg = $argExt->{arg}; + } + + # this option takes no arguments - it's a straight boolean + if ($optConfig->{cfgs} & OP_ARG_NONE) { + if ($arg) { + $return = 1; + } + else { + $return = 0; + } + } + + # if the option is present, it must have an argument. + # theoretically I should have code here actually requiring the argument, + # but at the moment that's being handled by Getopt::Long + elsif ($optConfig->{cfgs} & OP_ARG_REQ) { + if (!defined($arg)) { + # the opt wasn't specified at all. Perfectly legal, return undef + $return = undef; + } + else { + # if there was an arg provided, just return it + if (length($arg)) { + $return = $arg; + } + # No arg, but we were requested to prompt the user - do so + elsif ($optConfig->{cfgs} & OP_FROM_PROMPT) { + if (!exists($optConfig->{prompt})) { + ptrans(12, "Internal option processing error: option $argExt->{opt} missing required prompt key (this is a swaks bug)"); + exit(1); + } + if (!exists($optConfig->{match})) { + ptrans(12, "Internal option processing error: option $argExt->{opt} missing required match key (this is a swaks bug)"); + exit(1); + } + $return = interact($optConfig->{prompt}, $optConfig->{match}); + } + # No arg, no request to prompt - this is an error since we're requiring an arg + else { + ptrans(12, "Option processing error: option $argExt->{opt} specified with no argument"); + exit(1); + } + + # OP_FROM_FILE means that the above options might have resolved into '-' or @filename. If so, return the actual + # data contained in STDIN/@filename + if ($optConfig->{cfgs} & OP_FROM_FILE) { + if ($return eq '-') { + if (defined($G::stdin)) { + # multiple options can specify stdin, but we can only read it once. If anyone has + # already read stdin, provide the saved value here + $return = $G::stdin; + } + else { + $return = join('', ); + $G::stdin = $return; + } + } + elsif ($return =~ /^\@\@/) { + # if the argument starts with \@\@, we take that to mean that the user wants a literal value that starts + # with an @. The first @ is just an indicator, so strip it off before continuing + $return =~ s/^\@//; + } + elsif ($return =~ /^\@/) { + # a single @ means it's a filename. Open it and use the contents as the return value + $return =~ s/^\@//; + if (!open(F, "<$return")) { + ptrans(12, "Option processing error: file $return not openable for option $argExt->{opt} ($!)"); + exit(1); + } + $return = join('', ); + close(F); + } + + { + # All of the functionality in this bare block is deprecated, remove the whole thing later. + # if --data and single line, try to open it, error otherwise + # if !--data and is openable file, try to open and read, otherwise just use it as literal data + if ($argExt->{opt} eq 'data') { + # the old rule for --data was that anything that didn't have a newline in it would be treated + # as a file, and we would error if we couldn't open it. That would prevent us from sending + # typoed filenames as the data of messages + if ($return !~ m/\\n|\n|%NEWLINE%/) { + deprecate('Inferring a filename from the argument to --' . $argExt->{opt} . + ' will be removed in the future. Prefix filenames with \'@\' instead.'); + if (!open(F, "<$return")) { + ptrans(12, "$argExt->{opt} option appears to be a file but is not openable: $return ($!)"); + exit(1); + } + $return = join('', ); + close(F); + } + } + elsif (open(F, "<$return")) { + # the old rule for any other file option (--attach, --attach-body, --body) was that + # if it was openable, we would use the contents of the file, otherwise we would + # use the string itself + deprecate('Inferring a filename from the argument to --' . $argExt->{opt} . + ' will be removed in the future. Prefix filenames with \'@\' instead.'); + $return = join('', ); + close(F); + } + } + } + } + } + + # The option can be present with or without an argument + # any "true" return value will be an actual provided option + # false and defined = option given but no argument given + # false and undefined = option not specified + elsif ($optConfig->{cfgs} & OP_ARG_OPT) { + if (!defined($arg)) { + # the opt wasn't specified at all. Perfectly legal, return undef + $return = undef; + } + else { + # we have an opt and an arg, return the arg + $return = $arg; + } + } + + # if we read the last arg off an array, put it back on the array for future reads. I can't + # decide if this is the right behavior or not, but this makes it more like scalars, which + # can (and in a couple of cases, must) be read multiple times. + if (defined($arg) && ref($userOpts->{values}{$opt}) && !scalar(@{$userOpts->{values}{$opt}})) { + push(@{$userOpts->{values}{$opt}}, $argExt); + } + + # print "returning "; + # if (defined($return)) { + # print length($return) ? "$return\n" : "defined but empty\n"; + # } + # else { + # print "undefined\n"; + # } + return($return); } # A couple of global options are set in here, they will be in the G:: namespace sub process_args { - my $o = shift; # This is the args we got from command line - my %n = (); # This is the hash we will return w/ the fixed-up args - my $fconf = {}; # Hold config info from -l file if specified + my $o = shift; # This is the args we got from command line + my %n = (); # This is the hash we will return w/ the fixed-up args + my $a = get_option_struct(); # defining information for all options - # load the $fconf hash if user has specified a -l file - process_file($o->{option_file}, $fconf) if ($o->{option_file}); + # handle the output file handles early so they can be used for errors + # we don't need to keep track of the actual files but it will make debugging + # easier later + $G::trans_fh_oh = \*STDOUT; + $G::trans_fh_of = "STDOUT"; + $G::trans_fh_eh = \*STDERR; + $G::trans_fh_ef = "STDERR"; + my $output_file = get_arg('output_file', $o); + my $output_file_stderr = get_arg('output_file_stderr', $o) || $output_file; + my $output_file_stdout = get_arg('output_file_stdout', $o) || $output_file; + if ($output_file_stderr) { + if (!open(OUTEFH, '>>'.$output_file_stderr)) { + ptrans(12, 'Unable to open ' . $output_file_stderr . ' for writing'); + exit(1); + } + $G::trans_fh_eh = \*OUTEFH; + $G::trans_fh_ef = $output_file_stderr; + } + if ($output_file_stdout && $output_file_stdout eq $output_file_stderr) { + $G::trans_fh_oh = $G::trans_fh_eh; + $G::trans_fh_of = $G::trans_fh_ef; + } + elsif ($output_file_stdout) { + if (!open(OUTOFH, '>>'.$output_file_stdout)) { + ptrans(12, 'Unable to open ' . $output_file_stdout . ' for writing'); + exit(1); + } + $G::trans_fh_oh = \*OUTOFH; + $G::trans_fh_of = $output_file_stdout; + } - $G::dump_args = 1 if ($o->{dump_args}); - $G::suppress_data = 1 if ($o->{suppress_data}); - $G::no_hints = 1 if ($o->{no_hints}); - $G::hide_send = 1 if ($o->{hide_send}); - $G::hide_receive = 1 if ($o->{hide_receive}); - $G::pipeline = 1 if ($o->{pipeline}); - $G::silent = $o->{silent} ? $o->{silent} : 0; + if (get_arg('no_hints', $o)) { + $G::no_hints_send = 1; + $G::no_hints_recv = 1; + } + else { + $G::no_hints_send = get_arg('no_hints_send', $o); + $G::no_hints_recv = get_arg('no_hints_recv', $o); + } + $G::dump_mail = get_arg('dump_mail', $o); + $G::suppress_data = get_arg('suppress_data', $o); + $G::no_hints_info = get_arg('no_hints_info', $o); + $G::hide_send = get_arg('hide_send', $o); + $G::hide_receive = get_arg('hide_receive', $o); + $G::hide_informational = get_arg('hide_informational', $o); + $G::hide_all = get_arg('hide_all', $o); + $G::show_raw_text = get_arg('show_raw_text', $o); + $G::protect_prompt = get_arg('protect_prompt', $o); + $G::pipeline = get_arg('pipeline', $o); + $G::prdr = get_arg('prdr', $o); + $G::silent = get_arg('silent', $o) || 0; - my %protos = ( - smtp => { proto => 'smtp', auth => 0, tls => '0' }, - ssmtp => { proto => 'esmtp', auth => 0, tls => 'c' }, - ssmtpa => { proto => 'esmtp', auth => 1, tls => 'c' }, - smtps => { proto => 'smtp', auth => 0, tls => 'c' }, - esmtp => { proto => 'esmtp', auth => 0, tls => '0' }, - esmtpa => { proto => 'esmtp', auth => 1, tls => '0' }, - esmtps => { proto => 'esmtp', auth => 0, tls => 's' }, - esmtpsa => { proto => 'esmtp', auth => 1, tls => 's' }, - lmtp => { proto => 'lmtp', auth => 0, tls => '0' }, - lmtpa => { proto => 'lmtp', auth => 1, tls => '0' }, - lmtps => { proto => 'lmtp', auth => 0, tls => 's' }, - lmtpsa => { proto => 'lmtp', auth => 1, tls => 's' }, - ); - $G::protocol = lc($o->{mail_protocol}) || 'esmtp'; - if (!$protos{$G::protocol}) { - ptrans(12, "Unknown protocol $G::protocol specified, exiting"); - exit(1); - } - if ($protos{$G::protocol}{auth} && !$o->{auth_user} && !$o->{auth_pass} && - !$o->{auth_optional} && !$o->{auth}) - { - $o->{auth} = ''; # cause auth to be processed below - } - if ($protos{$G::protocol}{tls} && !$o->{tls} && !$o->{tls_optional} && - !$o->{tls_on_connect}) - { - if ($protos{$G::protocol}{tls} eq 's') { - $o->{tls} = ''; - } elsif ($protos{$G::protocol}{tls} eq 'c') { - $o->{tls_on_connect} = ''; - } - } - $G::protocol = $protos{$G::protocol}{proto}; + if (defined(my $dump_args = get_arg('dump_args', $o))) { + map { $G::dump_args{uc($_)} = 1; } (split('\s*,\s*', $dump_args)); # map comma-delim options into a hash + $G::dump_args{'ALL'} = 1 if (!scalar(keys(%G::dump_args))); # if no options were given, just set ALL + } - # set global option of -q option - if ($o->{quit_after}) { - $G::quit_after = lc($o->{quit_after}); - if ($G::quit_after =~ /^[el]hlo$/) { $G::quit_after = 'helo'; } - elsif ($G::quit_after =~ /first-[el]hlo/) { $G::quit_after = 'first-helo'; } - elsif ($G::quit_after eq 'starttls') { $G::quit_after = 'tls'; } - elsif ($G::quit_after eq 'from') { $G::quit_after = 'mail'; } - elsif ($G::quit_after eq 'to') { $G::quit_after = 'rcpt'; } - elsif ($G::quit_after ne 'connect' && $G::quit_after ne 'first-helo' && - $G::quit_after ne 'tls' && $G::quit_after ne 'helo' && - $G::quit_after ne 'auth' && $G::quit_after ne 'mail' && - $G::quit_after ne 'rcpt') - { - ptrans(12, "Unknown quit value $G::quit_after, exiting"); - exit(1); - } - # only rcpt _requires_ a to address - $G::server_only = 1 if ($G::quit_after ne 'rcpt'); - } else { - $G::quit_after = ''; - } + my $mail_server_t = get_arg('mail_server', $o); + my $socket_t = get_arg('socket', $o); + my $pipe_cmd_t = get_arg('pipe_cmd', $o); - # set global flag for -stl flag - $G::show_time_lapse = time() if (defined($o->{show_time_lapse})); - $G::show_time_hires = 1 if ($G::show_time_lapse && - avail("hires_timing") && - $o->{show_time_lapse} !~ /^i/i); + # it is an error if >1 of --server, --socket, or --pipe is specified + if ((defined($mail_server_t) && defined($socket_t)) || + (defined($mail_server_t) && defined($pipe_cmd_t)) || + (defined($pipe_cmd_t) && defined($socket_t))) + { + ptrans(12, "Multiple transport types specified, exiting"); + exit(1); + } - if ($o->{emulate_mail}) { # set up for -m option - $n{to} = shift if (!defined($o->{mail_to})); - $o->{mail_data} = ''; # define it here so we get it on stdin later - } + my %protos = ( + smtp => { proto => 'smtp', auth => 0, tls => '0' }, + ssmtp => { proto => 'esmtp', auth => 0, tls => 'c' }, + ssmtpa => { proto => 'esmtp', auth => 1, tls => 'c' }, + smtps => { proto => 'smtp', auth => 0, tls => 'c' }, + esmtp => { proto => 'esmtp', auth => 0, tls => '0' }, + esmtpa => { proto => 'esmtp', auth => 1, tls => '0' }, + esmtps => { proto => 'esmtp', auth => 0, tls => 's' }, + esmtpsa => { proto => 'esmtp', auth => 1, tls => 's' }, + lmtp => { proto => 'lmtp', auth => 0, tls => '0' }, + lmtpa => { proto => 'lmtp', auth => 1, tls => '0' }, + lmtps => { proto => 'lmtp', auth => 0, tls => 's' }, + lmtpsa => { proto => 'lmtp', auth => 1, tls => 's' }, + ); + $G::protocol = lc(get_arg('mail_protocol', $o)) || 'esmtp'; + my $tls = get_arg('tls', $o); + my $tls_optional = get_arg('tls_optional', $o); + my $tls_optional_strict = get_arg('tls_optional_strict', $o); + my $tls_on_connect = get_arg('tls_on_connect', $o); + if (!$protos{$G::protocol}) { + ptrans(12, "Unknown protocol $G::protocol specified, exiting"); + exit(1); + } + my $auth_user_t = get_arg('auth_user', $o); + my $auth_pass_t = get_arg('auth_pass', $o); + my $auth_optional_t = get_arg('auth_optional', $o); + my $auth_optional_strict_t = get_arg('auth_optional_strict', $o); + my $auth_t = get_arg('auth', $o); + if ($protos{$G::protocol}{auth} && !$auth_user_t && !$auth_pass_t && !$auth_optional_t && !$auth_optional_strict_t && !$auth_t) { + $auth_t = ''; # cause auth to be processed below + } + if ($protos{$G::protocol}{tls} && !$tls && !$tls_optional && !$tls_optional_strict && !$tls_on_connect){ + # 'touch' the variable so we process it below + if ($protos{$G::protocol}{tls} eq 's') { + $tls = 1; + } elsif ($protos{$G::protocol}{tls} eq 'c') { + $tls_on_connect = 1; + } + } + $G::protocol = $protos{$G::protocol}{proto}; - # pipe command, if one is specified - $G::link{process} = $o->{pipe_cmd} || interact("Pipe: ", '^.+$') - if (defined($o->{pipe_cmd})); - $G::link{process} ||= $fconf->{PIPE} || ""; - if ($G::link{process}) { $G::link{type} = 'pipe'; } - else { delete($G::link{process}); } + # set global options for --quit-after, --drop-after, and --drop-after-send + foreach my $opt ('quit_after', 'drop_after', 'drop_after_send') { + no strict "refs"; + if (my $value = get_arg($opt, $o)) { + ${"G::$opt"} = lc($value); + if (${"G::$opt"} =~ /^[el]hlo$/) { ${"G::$opt"} = 'helo'; } + elsif (${"G::$opt"} =~ /first-[el]hlo/) { ${"G::$opt"} = 'first-helo'; } + elsif (${"G::$opt"} eq 'starttls') { ${"G::$opt"} = 'tls'; } + elsif (${"G::$opt"} eq 'banner') { ${"G::$opt"} = 'connect'; } + elsif (${"G::$opt"} eq 'from') { ${"G::$opt"} = 'mail'; } + elsif (${"G::$opt"} eq 'to') { ${"G::$opt"} = 'rcpt'; } + elsif (${"G::$opt"} ne 'connect' && ${"G::$opt"} ne 'first-helo' && + ${"G::$opt"} ne 'tls' && ${"G::$opt"} ne 'helo' && + ${"G::$opt"} ne 'auth' && ${"G::$opt"} ne 'mail' && + ${"G::$opt"} ne 'rcpt' && ${"G::$opt"} ne 'xclient' && + ${"G::$opt"} ne 'data' && ${"G::$opt"} ne 'dot') + { + ptrans(12, "Unknown $opt value " . ${"G::$opt"} . ", exiting"); + exit(1); + } + # only rcpt, data, and dot _require_ a to address + $G::server_only = 1 if (${"G::$opt"} !~ /^(rcpt|data|dot)$/); - # socket file, if one is specified - $G::link{sockfile} = $o->{socket} || interact("Socket File: ", '^.+$') - if (defined($o->{socket})); - $G::link{sockfile} ||= $fconf->{SOCKET} || ""; - if ($G::link{sockfile}) { $G::link{type} = 'socket-unix'; } - else { delete($G::link{sockfile}); } + # data and dot aren't legal for quit_after + if ($opt eq 'quit_after' && (${"G::$opt"} eq 'data' || ${"G::$opt"} eq 'dot')) { + ptrans(12, "Unknown $opt value " . ${"G::$opt"} . ", exiting"); + exit(1); + } + } else { + ${"G::$opt"} = ''; + } + } - my $user = get_username($o->{force_getpwuid}); - my $hostname = get_hostname(); + # set global flag for -stl flag + $G::show_time_lapse = get_arg('show_time_lapse', $o); + if (defined($G::show_time_lapse)) { + if (length($G::show_time_lapse) && $G::show_time_lapse !~ /^i/i) { + ptrans(12, "Unknown argument '$G::show_time_lapse' to option show-time-lapse, exiting"); + exit(1); + } + if (avail("hires_timing") && $G::show_time_lapse !~ /^i/i) { + $G::show_time_lapse = 'hires'; + } + else { + $G::show_time_lapse = 'integer'; + } + } - # SMTP mail from - $n{from} = $o->{mail_from} || interact("From: ", '^.*$') - if (defined($o->{mail_from})); - $n{from} ||= $fconf->{FROM} || ($hostname || ($G::server_only && - $G::quit_after ne 'mail') - ? "$user\@$hostname" - : interact("From: ", '^.*$')); - $n{from} = '' if ($n{from} eq '<>'); + # pipe command, if one is specified + if ($pipe_cmd_t) { + $G::link{process} = $pipe_cmd_t; + $G::link{type} = 'pipe'; + } - # SMTP helo/ehlo - $n{helo} = $o->{mail_helo} || interact("Helo: ", '^.*$') - if (defined($o->{mail_helo})); - $n{helo} ||= $fconf->{HELO} || ($hostname || ($G::quit_after eq 'connect') - ? $hostname - : interact("Helo: ", '^.*$')); + # socket file, if one is specified + if ($socket_t) { + $G::link{sockfile} = $socket_t; + $G::link{type} = 'socket-unix'; + } - # SMTP server and rcpt to are interdependant, so they are handled together - $G::link{server} = $o->{mail_server} || interact("Server: ", '^.*$') - if (defined($o->{mail_server})); - $G::link{server} ||= $fconf->{SERVER}; - $n{to} = $o->{mail_to} || interact("To: ", '^.*$') - if (defined($o->{mail_to})); - $n{to} ||= $fconf->{TO}; - $n{to} = interact("To: ", '^.*$') - if (!$n{to} && !($G::server_only && ($G::link{server} || - $G::link{type} eq 'socket-unix' || - $G::link{type} eq 'pipe'))); - if (!$G::link{type}) { - $G::link{server} = get_server($n{to}) if (!$G::link{server}); - $G::link{type} = "socket-inet"; - } + $n{force_getpwuid} = get_arg('force_getpwuid', $o); # make available for --dump + my $user = get_username($n{force_getpwuid}); + my $hostname = get_hostname(); - # Verify we are able to handle the requested transport - if ($G::link{type} eq 'pipe') { - if (!avail("pipe")) { - ptrans(12, avail_str("pipe").". Exiting"); - exit(2); - } - } else { - if (!avail("socket")) { - ptrans(12, avail_str("socket").". Exiting"); - exit(2); - } - } + # SMTP mail from + if (!($n{from} = get_arg('mail_from', $o))) { + if ($hostname || ($G::server_only && $G::quit_after ne 'mail' && $G::drop_after ne 'mail' && $G::drop_after_send ne 'mail')) { + # if we have a hostname, or it doesn't matter anyway because we won't actually need it, use our manufactured from + $n{from} = "$user\@$hostname"; + } + else { + ptrans(12, "From string required but couldn't be determined automatically. Please use --from"); + exit(1); + } + } + $n{from} = '' if ($n{from} eq '<>'); - # local interface to connect from - $G::link{lint} = $o->{lint} || interact("Interface: ", '^.*$') - if (defined($o->{lint})); - $G::link{lint} ||= $fconf->{INTERFACE} || '0.0.0.0'; + # local interface and port + ($G::link{lint},$G::link{lport}) = parse_server(get_arg('lint', $o), get_arg('lport', $o)); + if ($G::link{lport} && $G::link{lport} !~ /^\d+$/) { + if (my $port = getservbyname($G::link{lport}, 'tcp')) { + $G::link{lport} = $port; + } + else { + ptrans(12, "unable to resolve service name $G::link{lport} into a port, exiting"); + exit(1); + } + } - # SMTP timeout - $o->{timeout} = '0s' if ($o->{timeout} eq '0'); # used 'eq' on purpose - $G::link{timeout} = $o->{timeout} || interact("Timeout: ", '^\d+[hHmMsS]?$') - if (defined($o->{timeout})); - $G::link{timeout} ||= $fconf->{TIMEOUT} || '30s'; - $G::link{timeout} = time_to_seconds($G::link{timeout}); + # SMTP helo/ehlo + if (!($n{helo} = get_arg('mail_helo', $o))) { + if ($hostname || ($G::quit_after eq 'connect' || $G::drop_after eq 'connect' || $G::drop_after_send eq 'connect')) { + # if we have a hostname, or it doesn't matter anyway because we won't actually need it, use our manufactured from + $n{helo} = $hostname; + } + else { + ptrans(12, "Helo string required but couldn't be determined automatically. Please use --helo"); + exit(1); + } + } - my $body = 'This is a test mailing'; # default message body - my $bound = ""; - my $stdin = undef; - if (defined($o->{body_822})) { - # the --body option is the entire 822 body and trumps and other options - # that mess with the body - if (!$o->{body_822}) { - $body = interact("Body: ", '.+'); - } elsif ($o->{body_822} eq '-') { - $stdin = join('', ); - $body = $stdin; - } else { - $body = $o->{body_822}; - } - if (open(I, "<$body")) { - $body = join('', ); - close(I); - } - } - if (scalar(@{$o->{attach_822}})) { - # this option is a list of files (or STDIN) to attach. In this case, - # the message become a mime message and the "body" goes in the - # first text/plain part - my $mime_type = 'application/octet-stream'; - my @parts = ( { body => $body, type => 'text/plain' } ); - $bound = "----=_MIME_BOUNDARY_000_$$"; - while (defined(my $t = shift(@{$o->{attach_822}}))) { - if ($t =~ m|^[^/]+/[^/]+$| && !stat($t)) { - $mime_type = $t; - } else { - push(@parts, { body => "$t", type => $mime_type }); - } - } - $body = ''; - foreach my $p (@parts) { - if ($p->{body} eq '-') { - if ($stdin) { - $p->{body} = $stdin; - } else { - $p->{body} = join('', ); - $stdin = $p->{body}; - } - } elsif (open(I, "<$p->{body}")) { - $p->{body} = join('', ); - close(I); - } - $body .= "--$bound\n" - . "Content-Type: $p->{type}\n"; - if ($p->{type} =~ m|^text/plain$|i) { - $body .= "\n" . $p->{body} . "\n"; - } else { - $body .= "Content-Transfer-Encoding: BASE64\n" - . "Content-Disposition: attachment\n" - . "\n" - . eb64($p->{body}, "\n") . "\n"; - } - } - $body .= "--$bound--\n"; - } + # SMTP server, port and rcpt-to are interdependant, so they are handled together + $G::link{type} ||= 'socket-inet'; + ($G::link{server},$G::link{port}) = parse_server($mail_server_t, get_arg('mail_port', $o)); + $n{to} = get_arg('mail_to', $o); + # we absolutely must have a recipient. If we don't have one yet, prompt for one + if (!$n{to} && !($G::server_only && ($G::link{server} || $G::link{type} eq 'socket-unix' || $G::link{type} eq 'pipe'))) { + $n{to} = interact("To: ", '^.+$'); # WCSXXXFIXME I wish we could look up the prompt and re from $a + } - # add-header option. In a strict technical sense all this is is a text - # string that will replace %H in the DATA. Because of where %H is placed - # in the default DATA, in practice this is used to add headers to the stock - # DATA w/o having to craft a custom DATA portion - #if (scalar(@{$o->{add_header}})) { - # $n{add_header} = join("\n", @{$o->{add_header}}) . "\n"; - #} - @{$o->{add_header}} = map { split(/\\n/) } @{$o->{add_header}}; + # try to catch obvious -s/-li/-4/-6 errors as soon as possible. We don't actually do any DNS + # lookups ourselves, so errors like -s being a domain with only A RRs and -li being a domain + # with only AAAA RRs, or -s being an ipv6 and -li being a domain with only A RRs, will + # get passed into the IO::Socket module to deal with and will just registed as a connection + # failure. + if ($G::link{type} eq 'socket-inet') { + my $forceIPv4 = get_arg('force_ipv4', $o); + my $forceIPv6 = get_arg('force_ipv6', $o); + if ($forceIPv4 && $forceIPv6) { + ptrans(12, "Options -4 and -6 are mutually exclusive, cannot proceed"); + exit 1; + } elsif ($forceIPv6) { + $G::link{force_ipv6} = 1; + } elsif ($forceIPv4) { + $G::link{force_ipv4} = 1; + } - # SMTP DATA - # a '-' arg to -d is the same as setting -g - if ($o->{mail_data} eq '-') { - undef($o->{mail_data}); - $o->{data_on_stdin} = 1; - } - if (defined($o->{mail_data}) && !defined($o->{data_on_stdin})) { - if (defined($o->{emulate_mail})) { - $n{data} = "Subject: " . interact("Subject: ", 'SKIP') . "\n\n"; - do { - $n{data} .= interact('', 'SKIP') . "\n"; - } while ($n{data} !~ /\n\.\n$/ms); - $n{data} =~ s/\n\.\n$//ms; - } else { - $n{data} = $o->{mail_data} || interact("Data: ", '^.*$'); - } - } - $n{data} ||= $fconf->{DATA} - || 'Date: %D\nTo: %T\nFrom: %F\nSubject: test %D\n' - ."X-Mailer: swaks v$p_version jetmore.org/john/code/#swaks".'\n' - . ($bound ? 'MIME-Version: 1.0\n' - .'Content-Type: multipart/mixed; ' - .'boundary="'.$bound.'"\n' - : '') - .'%H' # newline will be added in replacement if it exists - .'\n' - .'%B\n'; - # The -g option trumps all other methods of getting the data - $n{data} = join('', ) if ($o->{data_on_stdin}); - if (!$o->{no_data_fixup}) { - $n{data} =~ s/%B/$body/g; - if (scalar(@{$o->{header}})) { - my %matched = (); - foreach my $l (map { split(/\\n/) } @{$o->{header}}) { - if (my($h) = $l =~ /^([^:]+):/) { - if (!$matched{$h} && $n{data} =~ s/(^|\\n)$h:.*?($|\\n)/$1$l$2/) { - $matched{$h} = 1; - } else { push(@{$o->{add_header}}, $l); } - } else { push(@{$o->{add_header}}, $l); } - } - } - $n{add_header} = join('\n', @{$o->{add_header}}) . "\n" - if (@{$o->{add_header}}); - $n{data} =~ s/%H/$n{add_header}/g; - $n{data} =~ s/\\n/\r\n/g; - $n{data} =~ s/%F/$n{from}/g; - $n{data} =~ s/%T/$n{to}/g; - $n{data} =~ s/%D/get_date_string()/eg; - $n{data} =~ s/^From [^\n]*\n// if (!$O{no_strip_from}); - $n{data} =~ s/\r?\n\.\r?\n?$//s; # If there was a trailing dot, remove it - $n{data} =~ s/\n\./\n../g; # quote any other leading dots - # translate line endings - run twice to get consecutive \n correctly - $n{data} =~ s/([^\r])\n/$1\r\n/gs; - $n{data} =~ s/([^\r])\n/$1\r\n/gs; # this identical call not a bug - $n{data} .= "\r\n."; # add a trailing dot - } + if ($n{copy_routing} = get_arg('copy_routing', $o)) { + $G::link{server} ||= get_server($n{copy_routing}); + } + else { + $G::link{server} ||= get_server($n{to}); + } - # Handle TLS options - $G::tls_optional = 1 if (defined($o->{tls_optional})); - $G::tls = 1 if (defined($o->{tls}) || $G::tls_optional); - $G::tls_on_connect = 1 if (defined($o->{tls_on_connect})); - $G::link{tls}{active} = 0; - if ($G::tls || $G::tls_on_connect) { - if (!avail("tls")) { - if ($G::tls_optional) { - $G::tls = undef; # so we won't try it later - ptrans(12,avail_str("tls").". Skipping optional TLS"); - } else { - ptrans(12,avail_str("tls").". Exiting"); - exit(10); - } - } - } + if ($forceIPv4 && $G::link{server} =~ m|:|) { + ptrans(12, "Option -4 is set but server appears to be ipv6, cannot proceed"); + exit 1; + } elsif ($forceIPv4 && $G::link{lint} =~ m|:|) { + ptrans(12, "Option -4 is set but local-interface appears to be ipv6, cannot proceed"); + exit 1; + } elsif ($forceIPv6 && $G::link{server} =~ m|^\d+\.\d+\.\d+\.\d+$|) { + ptrans(12, "Option -6 is set but server appears to be ipv4, cannot proceed"); + exit 1; + } elsif ($forceIPv6 && $G::link{lint} =~ m|^\d+\.\d+\.\d+\.\d+$|) { + ptrans(12, "Option -6 is set but local-interface appears to be ipv4, cannot proceed"); + exit 1; + } elsif ($G::link{server} =~ m|:| && $G::link{lint} =~ m|^\d+\.\d+\.\d+\.\d+$|) { + ptrans(12, "server is ipv6 but local-interface is ipv4, cannot proceed"); + exit 1; + } elsif ($G::link{server} =~ m|^\d+\.\d+\.\d+\.\d+$| && $G::link{lint} =~ m|:|) { + ptrans(12, "server is ipv4 but local-interface is ipv6, cannot proceed"); + exit 1; + } + } - # SMTP port - $G::link{port} = $o->{mail_port} || interact("Port: ", '^\w+$') - if (defined($o->{mail_port})); - $G::link{port} ||= $fconf->{PORT}; - if ($G::link{port}) { - # in here, the user has either specified a port, or that they _want_ - # to, so if it isn't a resolvable port, ,keep prompting for another one - my $o_port = $G::link{port}; - if ($G::link{port} !~ /^\d+$/) { - $G::link{port} = getservbyname($G::link{port}, 'tcp'); - while (!$G::link{port}) { - $G::link{port} = $o_port = - interact("Unable to resolve port $o_port\nPort: ", '^\w+$'); - $G::link{port} = getservbyname($G::link{port}, 'tcp') - if ($G::link{port} !~ /^\d+$/); - } - } - } else { - # in here, user wants us to use default ports, so try look up services, - # use default numbers is service names don't resolve. Never prompt user - if ($G::protocol eq 'lmtp') { - $G::link{port} = getservbyname('lmtp', 'tcp') || '24'; - } elsif ($G::tls_on_connect) { - $G::link{port} = getservbyname('smtps', 'tcp') || '465'; - } else { - $G::link{port} = getservbyname('smtp', 'tcp') || '25'; - } - } - + # Verify we are able to handle the requested transport + if ($G::link{type} eq 'pipe') { + if (!avail("pipe")) { + ptrans(12, avail_str("pipe").". Exiting"); + exit(10); + } + } else { + if (!avail("socket")) { + ptrans(12, avail_str("socket").". Exiting"); + exit(10); + } elsif (($G::link{force_ipv6} || $G::link{server} =~ m|:| || $G::link{lint} =~ m|:|) && !avail("ipv6")) { + ptrans(12, avail_str("ipv6").". Exiting"); + exit(10); + } + } - # Handle AUTH options - $G::auth_optional = 1 if (defined($o->{auth_optional})); - $o->{auth_types} = []; - if ($o->{auth}) { - @{$o->{auth_types}} = map { uc($_) } (split(/,/, $o->{auth})); - } elsif ($o->{auth_optional}) { - @{$o->{auth_types}} = map { uc($_) } (split(/,/, $o->{auth_optional})); - } elsif (defined($o->{auth_user}) || defined($o->{auth_pass}) || - $G::auth_optional || (defined($o->{auth}) && !$o->{auth})) - { - $o->{auth_types}[0] = 'ANY'; - $o->{auth} = 'ANY'; # this is checked below - } - # if after that processing we've defined some auth type, do some more - # specific processing - if (scalar(@{$o->{auth_types}})) { - # there's a lot of option processing below. If any type looks like it - # will succeed later, set this to true - my $valid_auth_found = 0; + # SMTP timeout + $G::link{timeout} = time_to_seconds(get_arg('timeout', $o) // '30s'); - # handle the --auth-map options plus our default mappings - foreach (split(/\s+,\s+/, $o->{auth_map}),"PLAIN=PLAIN","LOGIN=LOGIN", - "CRAM-MD5=CRAM-MD5","DIGEST-MD5=DIGEST-MD5","CRAM-SHA1=CRAM-SHA1", - "NTLM=NTLM","SPA=NTLM","MSN=NTLM") - { - my($alias,$type) = split(/=/, uc($_), 2); - # this gives us a list of all aliases and what the alias - $G::auth_map_f{$alias} = $type; - # this gives a list of all base types and any aliases for it. - $G::auth_map_t{$type} ||= []; - push(@{$G::auth_map_t{$type}}, $alias); - } - if (!avail("auth")) { # check for general auth requirements - if ($G::auth_optional) { - ptrans(12, avail_str("auth"). ". Skipping optional AUTH"); - } else { - ptrans(12, avail_str("auth"). ". Exiting"); - exit(10); - } - } else { - # if the user doesn't specify an auth type, create a list from our - # auth-map data. Simplifies processing later - if ($o->{auth_types}[0] eq 'ANY') { - $o->{auth_types} = [sort keys %G::auth_map_f]; - } + my $dab_sp = get_arg('dab_sp', $o); + my $dump_as_body = get_arg('dump_as_body', $o); + $dump_as_body = '' if ($dab_sp && !defined($dump_as_body)); + my $body = 'This is a test mailing'; # default message body + $body = 'DUMP_AS_BODY_HAS_BEEN_SET' if (defined($dump_as_body)); + my $bound = ''; + my $main_content_type = 'multipart/mixed'; + my $stdin = undef; + if (defined(my $body_822_t = get_arg('body_822', $o))) { + # the --body option is the entire 822 body and trumps any other options + # that mess with the body + $body = $body_822_t; + } + my $attach_accum = get_accum('attach_accum', $o); + if (scalar(@$attach_accum)) { + # this option is a list of files (or STDIN) to attach. In this case, + # the message become a mime message and the "body" goes in the + # first text/plain part + my $mime_type = '%SWAKS_DEFAULT_MIMETYTPE%'; + my $next_name = undef(); + my %parts = ( body => [], rest => [] ); + $bound = "----=_MIME_BOUNDARY_000_$$"; + foreach my $part (@$attach_accum) { + if ($part eq 'attach_type') { + $mime_type = get_arg($part, $o); + } + elsif ($part eq 'attach_name') { + $next_name = get_arg($part, $o); + } + elsif ($part eq 'attach_body') { + if ($mime_type eq '%SWAKS_DEFAULT_MIMETYTPE%') { + $mime_type = 'text/plain'; + } + push(@{$parts{body}}, { body => get_arg($part, $o), type => $mime_type }); + $next_name = undef(); # can't set filename for body, unset next_name so random attachment doesn't get it + $mime_type = '%SWAKS_DEFAULT_MIMETYTPE%'; # after each body, reset the default mime type + } + elsif ($part eq 'attach_attach') { + my $name = peek_arg($part, $o); + my $tpart = { body => get_arg($part, $o), type => $mime_type }; + if (defined($next_name)) { + $tpart->{name} = $next_name; + $next_name = undef(); + } + else { + my $filename = $name; + $filename =~ s/^\@//; + if ($name ne '-' && $filename !~ /^\@/ && $name ne $tpart->{body} && -f $filename) { + # name will have the unprocessed arg. If we think it came from a file, use the filename for + # the attachment name. Not super happy with this logic, try to improve in the future + ($tpart->{name}) = $name =~ m|/?([^/]+)$|; + } + } + push(@{$parts{rest}}, $tpart); + } else { + ptrans(12, "Error processing attach args, unknown type $part when processing attachment options"); + exit(1); + } + } - foreach my $type (@{$o->{auth_types}}) { - # we need to evaluate whether we will be able to run the auth types - # specified by the user + # if no body parts were set via --attach-body, set a text/plain body to $body + if (!scalar(@{$parts{body}})) { + push(@{$parts{body}}, { body => $body, type => 'text/plain' }); + } - if (!$G::auth_map_f{$type}) { - ptrans(12, "$type is not a recognized auth type, skipping"); - } + $body = ''; + if (!scalar(@{$parts{rest}})) { + # if there are no non-body parts + if (scalar(@{$parts{body}}) > 1) { + $main_content_type = 'multipart/alternative'; + } + else { + $main_content_type = 'multipart/mixed'; + } - elsif ($G::auth_map_f{$type} eq 'CRAM-MD5' && !avail("auth_cram_md5")) - { - ptrans(12, avail_str("auth_cram_md5")) if ($o->{auth} ne 'ANY'); - } + foreach my $part (@{$parts{body}}) { + $body .= encode_mime_part($part, $bound, 1); + } + } + else { + # otherwise, there's a mixture of both body and other. multipart/mixed + $main_content_type = 'multipart/mixed'; + if (scalar(@{$parts{body}}) > 1) { + # we have multiple body parts, plus other attachments. Need to create a mp/mixes mime object for the bodies + my $mp_bound = "----=_MIME_BOUNDARY_004_$$"; - elsif ($G::auth_map_f{$type} eq 'CRAM-SHA1' && !avail("auth_cram_sha1")) - { - ptrans(12, avail_str("auth_cram_sha1")) if ($o->{auth} ne 'ANY'); - } + $body .= "--$bound\n" + . 'Content-Type: multipart/alternative; boundary="' . $mp_bound . '"' . "\n\n"; - elsif ($G::auth_map_f{$type} eq 'NTLM' && !avail("auth_ntlm")) - { - ptrans(12, avail_str("auth_ntlm")) if ($o->{auth} ne 'ANY'); - } + foreach my $part (@{$parts{body}}) { + $body .= encode_mime_part($part, $mp_bound, 1); + } + $body .= "--$mp_bound--\n"; + } + else { + $body .= encode_mime_part($parts{body}[0], $bound, 1); + } - elsif ($G::auth_map_f{$type} eq 'DIGEST-MD5' && - !avail("auth_digest_md5")) - { - ptrans(12, avail_str("auth_digest_md5")) if ($o->{auth} ne 'ANY'); - } + # now handle the non-body attachments + foreach my $part (@{$parts{rest}}) { + $body .= encode_mime_part($part, $bound); + } + } + $body .= "--$bound--\n"; + } + $body =~ s|%SWAKS_DEFAULT_MIMETYTPE%|application/octet-stream|g; - else { - $valid_auth_found = 1; - push(@{$n{a_type}}, $type); - } + # SMTP DATA + $n{data} = get_arg('mail_data', $o) || + 'Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\n' . + "Message-Id: <%MESSAGEID%>\n" . + "X-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/".'\n' . + ($bound ? 'MIME-Version: 1.0\nContent-Type: ' . $main_content_type . '; boundary="' . $bound. '"\n' : '') . + '%NEW_HEADERS%' . # newline will be added in replacement if it exists + '\n' . + '%BODY%\n'; + if (!get_arg('no_data_fixup', $o)) { + $n{data} =~ s/%BODY%/$body/g; + $n{data} =~ s/\\n/\r\n/g; + my $addHeader_accum = get_accum('add_header', $o); + my $addHeaderOpt = []; - } # foreach + foreach my $okey (@$addHeader_accum) { + push(@$addHeaderOpt, get_arg($okey, $o)); + } - if (!$valid_auth_found) { - ptrans(12, "No auth types supported"); - if (!$G::auth_optional) { - exit(10); - } - $n{a_user} = $n{a_pass} = $n{a_type} = undef; - } else { - $n{a_user} = $o->{auth_user} if (defined($o->{auth_user})); - $n{a_user} ||= $fconf->{USER}; - $n{a_user} ||= interact("Username: ", 'SKIP'); - $n{a_user} = '' if ($n{a_user} eq '<>'); + # split the headers off into their own struct temporarily to make it much easier to manipulate them + my $header; + my @headers = (); + my %headers = (); - $n{a_pass} = $o->{auth_pass} if (defined($o->{auth_pass})); - $n{a_pass} ||= $fconf->{PASS}; - $n{a_pass} ||= interact("Password: ", 'SKIP'); - $n{a_pass} = '' if ($n{a_pass} eq '<>'); + # cut the headers off of the data + if ($n{data} =~ s/\A(.*?)\r?\n\r?\n//s) { + $header = $1; + } + else { + $header = $n{data}; + $n{data} = ''; + } - $G::auth_showpt = 1 if (defined($o->{auth_showpt})); - # This option is designed to hide passwords - turn echo off when - # supplying at PW prompt, star out the PW strings in AUTH transactions. - # Not implementing right now - the echo might be a portability issue, - # and starring out is hard because the smtp transaction is abstracted - # beyond where this is easy to do. Maybe sometime in the future - #$G::auth_hidepw = 1 if (defined($o->{auth_hidepw})); - } - } # end avail("auth") - } # end auth parsing + # build the header string into an object. Each header is an array, each index is a line (to handle header continuation lines) + foreach my $headerLine (split(/\r?\n/, $header)) { + if ($headerLine =~ /^\s/) { + # continuation line + if (scalar(@headers)) { + push(@{$headers[-1]}, $headerLine); + } + else { + # it's illegal to have a continuation line w/o a previous header, but we're a test tool + push(@headers, [ $headerLine ]); + } + } + elsif ($headerLine =~ /^(\S[^:]+):/) { + # properly formed header + push(@headers, [ $headerLine ]); + $headers{$1} = $headers[-1]; + } + else { + # malformed header - no colon. Allow it anyway, we're a test tool + push(@headers, [ $headerLine ]); + $headers{$headerLine} = $headers[-1]; + } + } - return(\%n); + # If the user specified headers and the header exists, replace it. If not, push it onto add_header to be added as new + my $header_accum = get_accum('header', $o); + my $headerOpt = []; + foreach my $okey (@$header_accum) { + push(@$headerOpt, get_arg($okey, $o)); + } + foreach my $headerLine (map { split(/\\n/) } @{$headerOpt}) { + if (my($headerName) = $headerLine =~ /^([^:]+):/) { + if ($headers{$headerName}) { + $headers{$headerName}[0] = $headerLine; + splice(@{$headers{$headerName}}, 1); # remove from index 1 onward, if they existed (possible continuations) + } + else { + push(@{$addHeaderOpt}, $headerLine); + } + } + else { + push(@{$addHeaderOpt}, $headerLine); + } + } + + # rebuild the header using our (possibly replaced) headers + my $newHeader = ''; + foreach my $headerObj (@headers) { + foreach my $line (@$headerObj) { + $newHeader .= $line . "\r\n"; + } + } + + # if there are new headers, add them as appropriate + if ($newHeader =~ /%NEW_HEADERS%/) { + $n{add_header} = join("\r\n", @{$addHeaderOpt}) . "\r\n" if (@{$addHeaderOpt}); + $newHeader =~ s/%NEW_HEADERS%/$n{add_header}/g; + } elsif (scalar(@{$addHeaderOpt})) { + foreach my $line (@{$addHeaderOpt}) { + $newHeader .= $line . "\r\n"; + } + } + + # Now re-assemble our data by adding the headers back on to the front + $n{data} = $newHeader . "\r\n" . $n{data}; + + $n{data} =~ s/\\n|%NEWLINE%/\r\n/g; + $n{data} =~ s/%FROM_ADDRESS%/$n{from}/g; + $n{data} =~ s/%TO_ADDRESS%/$n{to}/g; + $n{data} =~ s/%MESSAGEID%/get_messageid()/ge; + $n{data} =~ s/%SWAKS_VERSION%/$p_version/g; + $n{data} =~ s/%DATE%/get_date_string()/ge; + $n{data} =~ s/^From [^\n]*\n// if (!get_arg('no_strip_from', $o)); + $n{data} =~ s/\r?\n\.\r?\n?$//s; # If there was a trailing dot, remove it + $n{data} =~ s/\n\./\n../g; # quote any other leading dots + $n{data} =~ s/([^\r])\n/$1\r\n/gs; + $n{data} =~ s/([^\r])\n/$1\r\n/gs; # this identical call is not a bug, called twice to get consecutive \n correctly + $n{data} .= "\r\n."; # add a trailing dot + } + + # Handle TLS options + # tls => 0 - no. STARTTLS must be advertised and must succeed, else error. + # 1 - yes. Success if not advertised, advertised and fails _or_ succeeds. + # 2 - strict. Satisfied if not advertised, or advertised and succeeded. + # However, if it's advertised and fails, it's an error. + $G::tls_optional = 1 if ($tls_optional); + $G::tls_optional = 2 if ($tls_optional_strict); + $G::tls = 1 if ($tls || $G::tls_optional); + $G::tls_on_connect = 1 if ($tls_on_connect); + $G::link{tls}{active} = 0; + if ($G::tls || $G::tls_on_connect) { + if (!avail("tls")) { + if ($G::tls_optional) { + $G::tls = undef; # so we won't try it later + ptrans(12,avail_str("tls")); + } else { + ptrans(12,avail_str("tls").". Exiting"); + exit(10); + } + } + $G::tls_verify = get_arg('tls_verify', $o); + $G::tls_sni_hostname = get_arg('tls_sni_hostname', $o); + $G::tls_cipher = get_arg('tls_cipher', $o); + $G::tls_cert = get_arg('tls_cert', $o); + $G::tls_key = get_arg('tls_key', $o); + if (($G::tls_cert || $G::tls_key) && !($G::tls_cert && $G::tls_key)) { + ptrans(12, "--tls-cert and --tls-key require each other. Exiting"); + exit(1); + } + if (($G::tls_ca_path = get_arg('tls_ca_path', $o)) && !-f $G::tls_ca_path && !-d $G::tls_ca_path) { + ptrans(12, "--tls-ca-path: $G::tls_ca_path is not a valid file or directory. Exiting."); + exit(1); + } + + # this is kind of a kludge. There doesn't appear to be a specific openssl call to find supported + # protocols, but the OP_NO_protocol functions exist for supported protocols. Loop through + # "known" protocols (which will unfortunately need to be added-to by hand when new protocols + # become available) to find out which of them are available (when adding new types here, see + # also the code that calls Net::SSLeay::version() and translates to a readable value + @G::tls_supported_protocols = (); + foreach my $p (qw(SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3)) { + eval { no strict "refs"; &{"Net::SSLeay::OP_NO_$p"}(); }; + push(@G::tls_supported_protocols, $p) if (!$@); + } + + if (my $tls_protocols = get_arg('tls_protocol', $o)) { + @G::tls_protocols = (); + my @requested = split(/,\s*/, $tls_protocols); + if (my $c = scalar(grep(/^no_/i, @requested))) { + if ($c != scalar(@requested)) { + ptrans(12, "cannot mix X and no_X forms in --tls-protocol option"); + exit(1); + } + } + foreach my $p (@requested) { + my $t = $p; + $t =~ s/^no_//i; + if (grep /^$t$/i, @G::tls_supported_protocols) { + push(@G::tls_protocols, $p); + } else { + ptrans(12, "$p in --tls-protocol is not a known/supported protocol"); + } + } + if (!scalar(@G::tls_protocols)) { + ptrans(12, "no valid arguments provided to --tls-protocol, exiting"); + exit(1); + } + } + + $G::tls_get_peer_cert = get_arg('tls_get_peer_cert', $o); + $G::tls_get_peer_cert = 'STDOUT' if (defined($G::tls_get_peer_cert) && !length($G::tls_get_peer_cert)); + } + + # SMTP port + if ($G::link{port}) { + if ($G::link{port} !~ /^\d+$/) { + if (my $port = getservbyname($G::link{port}, 'tcp')) { + $G::link{port} = $port; + } + else { + ptrans(12, "unable to resolve service name $G::link{port} into a port, exiting"); + exit(1); + } + } + } else { + # in here, user wants us to use default ports, so try look up services, + # use default numbers is service names don't resolve. Never prompt user + if ($G::protocol eq 'lmtp') { + $G::link{port} = getservbyname('lmtp', 'tcp') || '24'; + } elsif ($G::tls_on_connect) { + $G::link{port} = getservbyname('smtps', 'tcp') || '465'; + } else { + $G::link{port} = getservbyname('smtp', 'tcp') || '25'; + } + } + + # XCLIENT + { # Create a block for local variables + $G::xclient{try} = 0; + $G::xclient{attr} = {}; + $G::xclient{strings} = []; + my @pieces = (); + my $xclient_accum = get_accum('xclient_accum', $o); + foreach my $attr (@$xclient_accum) { + if ($attr eq 'xclient_delim' || $attr eq 'xclient_raw') { + if (scalar(@pieces)) { + push(@{$G::xclient{strings}}, join(' ', @pieces)); + @pieces = (); + } + + if ($attr eq 'xclient_raw') { + push(@{$G::xclient{strings}}, get_arg('xclient_raw', $o)); + } + } else { + if (my $value = get_arg($attr, $o)) { + $attr =~ /^xclient_(.*)$/; + my $name = uc($1); + $G::xclient{attr}{$name} = 1; # used later to verify that we haven't asked for an un-advertised attr + push(@pieces, $name . '=' . to_xtext($value)); + } + } + } + push(@{$G::xclient{strings}}, join(' ', @pieces)) if (scalar(@pieces)); + $G::xclient{no_verify} = get_arg('xclient_no_verify', $o); + $G::xclient{optional} = get_arg('xclient_optional', $o); + $G::xclient{optional} = 2 if (get_arg('xclient_optional_strict', $o)); + #$G::xclient{after} = $o->{"xclient_after"} || interact("XCLIENT quit after: ", '^.+$') + # if (defined($o->{"xclient_after"})); + $G::xclient{try} = 1 if (scalar(@{$G::xclient{strings}})); + $G::xclient{before_tls} = get_arg('xclient_before_starttls', $o); + } + + # PROXY + $G::proxy{try} = 0; + $G::proxy{attr} = {}; + $G::proxy{version} = get_arg('proxy_version', $o); + $G::proxy{raw} = get_arg('proxy_raw', $o); + foreach my $attr ('family', 'source', 'source_port', 'dest', 'dest_port', 'protocol', 'command') { + if (my $val = get_arg('proxy_' . $attr, $o)) { + if ($G::proxy{raw}) { + ptrans(12, "Can't mix --proxy option with other --proxy-* options"); + exit(35); + } + $G::proxy{attr}{$attr} = $val; + } + } + if ($G::proxy{version}) { + if ($G::proxy{version} != 1 && $G::proxy{version} != 2) { + ptrans(12, "Invalid argument to --proxy: $G::proxy{version} is not a legal proxy version"); + exit(35); + } + } + else { + $G::proxy{version} = 1; + } + $G::proxy{try} = 1 if ($G::proxy{raw} || scalar(keys(%{$G::proxy{attr}}))); + if ($G::proxy{try} && !$G::proxy{raw}) { + $G::proxy{attr}{protocol} ||= 'STREAM'; + $G::proxy{attr}{command} ||= 'PROXY'; + foreach my $attr ('family', 'source', 'source_port', 'dest', 'dest_port', 'protocol', 'command') { + if (!$G::proxy{attr}{$attr}) { + ptrans(12, "Incomplete set of --proxy-* options (missing $attr)"); + exit(35); + } + $G::proxy{attr}{$attr} = uc($G::proxy{attr}{$attr}); + } + if ($G::proxy{attr}{protocol} !~ /^(UNSPEC|STREAM|DGRAM)$/) { + ptrans(12, 'unknown --proxy-protocol argument ' . $G::proxy{attr}{protocol}); + exit(35); + } + if ($G::proxy{attr}{command} !~ /^(LOCAL|PROXY)$/) { + ptrans(12, 'unknown --proxy-command argument ' . $G::proxy{attr}{command}); + exit(35); + } + if ($G::proxy{version} == 2 && $G::proxy{attr}{family} !~ /^(AF_UNSPEC|AF_INET|AF_INET6|AF_UNIX)$/) { + ptrans(12, 'unknown --proxy-family argument ' . $G::proxy{attr}{family} . ' for version 2'); + exit(35); + } + if ($G::proxy{version} == 1 && $G::proxy{attr}{family} !~ /^(TCP4|TCP6)$/) { + ptrans(12, 'unknown --proxy-family argument ' . $G::proxy{attr}{family} . ' for version 1'); + exit(35); + } + } + + # Handle AUTH options + # auth_optional => 0 - no. Auth must be advertised and must succeed, else error. + # 1 - yes. Success if not advertised, advertised and fails _or_ succeeds. + # 2 - strict. Satisfied if not advertised, or advertised and succeeded. + # However, if it's advertised and fails, it's an error. + $G::auth_optional = 1 if (defined($auth_optional_t)); + $G::auth_optional = 2 if (defined($auth_optional_strict_t)); + my $auth_types_t = []; + if ($auth_t) { + @{$auth_types_t} = map { uc($_) } (split(/,/, $auth_t)); + } elsif ($auth_optional_strict_t) { + @{$auth_types_t} = map { uc($_) } (split(/,/, $auth_optional_strict_t)); + } elsif ($auth_optional_t) { + @{$auth_types_t} = map { uc($_) } (split(/,/, $auth_optional_t)); + } elsif (defined($auth_user_t) || defined($auth_pass_t) || $G::auth_optional || (defined($auth_t) && !$auth_t)) { + $auth_types_t->[0] = 'ANY'; + $auth_t = 'ANY'; # this is checked below + $G::auth_type = 'ANY'; + } + # if after that processing we've defined some auth type, do some more + # specific processing + if (scalar(@{$auth_types_t})) { + # there's a lot of option processing below. If any type looks like it + # will succeed later, set this to true + my $valid_auth_found = 0; + + # handle the --auth-map options plus our default mappings + foreach (split(/\s*,\s*/, get_arg('auth_map', $o)),"PLAIN=PLAIN","LOGIN=LOGIN", + "CRAM-MD5=CRAM-MD5","DIGEST-MD5=DIGEST-MD5", + "CRAM-SHA1=CRAM-SHA1","NTLM=NTLM","SPA=NTLM","MSN=NTLM") + { + if (/^([^=]+)=(.+)$/) { + my($alias,$type) = ($1,$2); + $G::auth_map_f{$alias} = $type; # this gives us a list of all aliases pointing to types + $G::auth_map_t{$type} ||= []; # this gives a list of all base types and any aliases for it. + push(@{$G::auth_map_t{$type}}, $alias); + } else { + ptrans(12, "Unknown auth-map format '$_'"); + exit(1); + } + } + # Now handle the --auth-extra options + foreach (split(/\s*,\s*/, get_arg('auth_extra', $o))) { + if (/^([^=]+)=(.+)$/) { + $G::auth_extras{uc($1)} = $2; + } else { + ptrans(12, "Unknown auth-extra format '$_'"); + exit(1); + } + } + # handle the realm/domain synonyms + if ($G::auth_extras{DOMAIN}) { + $G::auth_extras{REALM} = $G::auth_extras{DOMAIN}; + } elsif ($G::auth_extras{DOMAIN}) { + $G::auth_extras{DOMAIN} = $G::auth_extras{REALM}; + } + if (!avail("auth")) { # check for general auth requirements + if ($G::auth_optional == 2) { + # we don't know yet if this is really an error. If the server + # doesn't advertise auth, then it's not really an error. So just + # save it in case we need it later + $G::auth_unavailable = avail_str("auth"); + ptrans(12, avail_str("auth")); + } elsif ($G::auth_optional == 1) { + ptrans(12, avail_str("auth"). ". Skipping optional AUTH"); + } else { + ptrans(12, avail_str("auth"). ". Exiting"); + exit(10); + } + } else { + # if the user doesn't specify an auth type, create a list from our + # auth-map data. Simplifies processing later + if ($auth_types_t->[0] eq 'ANY') { + $auth_types_t = [sort keys %G::auth_map_f]; + } + + foreach my $type (@{$auth_types_t}) { + # we need to evaluate whether we will be able to run the auth types + # specified by the user + if (!$G::auth_map_f{$type}) { + ptrans(12, "$type is not a recognized auth type, skipping"); + } elsif ($G::auth_map_f{$type} eq 'CRAM-MD5' && !avail("auth_cram_md5")) { + ptrans(12, avail_str("auth_cram_md5")) if ($auth_t ne 'ANY'); + } elsif ($G::auth_map_f{$type} eq 'CRAM-SHA1' && !avail("auth_cram_sha1")) { + ptrans(12, avail_str("auth_cram_sha1")) if ($auth_t ne 'ANY'); + } elsif ($G::auth_map_f{$type} eq 'NTLM' && !avail("auth_ntlm")) { + ptrans(12, avail_str("auth_ntlm")) if ($auth_t ne 'ANY'); + } elsif ($G::auth_map_f{$type} eq 'DIGEST-MD5' && !avail("auth_digest_md5")) { + ptrans(12, avail_str("auth_digest_md5")) if ($auth_t ne 'ANY'); + } else { + $valid_auth_found = 1; + push(@{$n{a_type}}, $type); + } + } + + if (!$valid_auth_found) { + ptrans(12, "No auth types supported"); + if ($G::auth_optional == 2) { + $G::auth_unavailable .= "No auth types supported"; + } elsif ($G::auth_optional == 1) { + $n{a_user} = $n{a_pass} = $n{a_type} = undef; + } else { + exit(10); + } + } else { + $auth_user_t ||= obtain_from_netrc('login'); + if (!$auth_user_t) { + my $cfg = { cfgs => OP_ARG_REQ|OP_FROM_PROMPT, prompt => 'Username: ', match => 'SKIP', okey => 'auth_user', akey => 'auth_user' }; + $auth_user_t = get_arg('auth_user', $o, $cfg, 1); + } + $n{a_user} = $auth_user_t eq '<>' ? '' : $auth_user_t; + + $auth_pass_t ||= obtain_from_netrc('password', $n{a_user}); + if (!$auth_pass_t) { + my $cfg = { cfgs => OP_ARG_REQ|OP_FROM_PROMPT, prompt => 'Password: ', match => 'SKIP', okey => 'auth_pass', akey => 'auth_pass' }; + $auth_pass_t = get_arg('auth_pass', $o, $cfg, 1); + } + $n{a_pass} = $auth_pass_t eq '<>' ? '' : $auth_pass_t; + + $G::auth_showpt = get_arg('auth_showpt', $o); + $G::auth_hidepw = get_arg('auth_hidepw', $o); + if (defined($G::auth_hidepw) && !$G::auth_hidepw) { + $G::auth_hidepw = 'PROVIDED_BUT_REMOVED'; + } + } + } # end avail("auth") + } # end auth parsing + + # the very last thing we do is swap out the body if --dump-as-body used + if (defined($dump_as_body)) { + if ($dump_as_body) { + $dump_as_body = uc($dump_as_body); + $dump_as_body =~ s/\s//g; + map { $G::dump_as_body{$_} = 1; } (split(',', $dump_as_body)); + } + else { + $G::dump_as_body{'ALL'} = 1; + } + + $n{data} =~ s|DUMP_AS_BODY_HAS_BEEN_SET|get_running_state(\%n, \%G::dump_as_body, {SUPPORT => 1, DATA => 1})|e; + if ($dab_sp) { + $n{data} =~ s|'%RAW_PASSWORD_STRING%'|shquote($n{a_pass})|eg; + } elsif ($G::auth_hidepw) { + $n{data} =~ s|'%RAW_PASSWORD_STRING%'|shquote($G::auth_hidepw)|eg; + } else { + $n{data} =~ s|'%RAW_PASSWORD_STRING%'|shquote('PROVIDED_BUT_REMOVED')|eg; + } + } + + return(\%n); +} + +sub encode_mime_part { + my $part = shift; + my $boundary = shift; + my $no_attach_text = shift; # if this is true and there's no name, Don't set disposition to attachment + my $text = ''; + + $text .= "--$boundary\n"; + if ($part->{type} =~ m|^text/plain$|i && !$part->{name}) { + $text .= "Content-Type: $part->{type}\n\n" . $part->{body} . "\n"; + } + else { + if ($part->{name}) { + $text .= "Content-Type: $part->{type}; name=\"$part->{name}\"\n" + . "Content-Description: $part->{name}\n" + . "Content-Disposition: attachment; filename=\"$part->{name}\"\n"; + } + else { + $text .= "Content-Type: $part->{type}\n"; + if (!($part->{type} =~ m|^text/|i && $no_attach_text)) { + $text .= "Content-Disposition: attachment\n"; + } + } + $text .= "Content-Transfer-Encoding: BASE64\n" + . "\n" . eb64($part->{body}, "\n") . "\n"; + } + + + return($text); +} + +sub parse_server { + my $server = shift; + my $port = shift; + + if ($server =~ m|^\[([^\]]+)\]:(.*)$|) { + # [1.2.3.4]:25 + # [hostname]:25 + # [1:2::3]:25 + return($1, $2); + } elsif ($server =~ m|^([^:]+):([^:]+)$|) { + # 1.2.3.4:25 + # hostname:25 + return($1, $2); + } elsif ($server =~ m|^\[?([^/\]]*)\]?/(\w+)$|) { + # 1.2.3.4/25 [1.2.3.4]/25 + # hostname/25 [hostname]/25 + # 1:2::3/25 [1:2::3]/25 + return($1, $2); + } elsif ($server =~ m|^\[([^\]]+)\]$|) { + # [1.2.3.4] + # [hostname] + # [1:2::3] + return($1, $port); + } + return($server, $port); +} + +sub get_running_state { + my $opts = shift; + my $dump_args = shift; + my $skip = shift; + my @parts = (); + + if (($dump_args->{'SUPPORT'} || $dump_args->{'ALL'}) && !$skip->{'SUPPORT'}) { + push(@parts, test_support(1)); + } + + if ($dump_args->{'APP'} || $dump_args->{'ALL'}) { + push(@parts, [ + 'App Info:', + " X-Mailer = $p_name v$p_version jetmore.org/john/code/swaks/", + ' Cmd Line = ' . $0 . ' ' . $G::cmdline, + ]); + } + + if ($dump_args->{'OUTPUT'} || $dump_args->{'ALL'}) { + push(@parts, [ + 'Output Info:', + ' show_time_lapse = ' . ($G::show_time_lapse ? "TRUE ($G::show_time_lapse)" : 'FALSE'), + ' show_raw_text = ' . ($G::show_raw_text ? 'TRUE' : 'FALSE'), + ' suppress_data = ' . ($G::suppress_data ? 'TRUE' : 'FALSE'), + ' protect_prompt = ' . ($G::protect_prompt ? 'TRUE' : 'FALSE'), + ' no_hints_send = ' . ($G::no_hints_send ? 'TRUE' : 'FALSE'), + ' no_hints_recv = ' . ($G::no_hints_recv ? 'TRUE' : 'FALSE'), + ' no_hints_info = ' . ($G::no_hints_info ? 'TRUE' : 'FALSE'), + " silent = $G::silent", + ' dump_mail = ' . ($G::dump_mail ? 'TRUE' : 'FALSE'), + ' hide_send = ' . ($G::hide_send ? 'TRUE' : 'FALSE'), + ' hide_receive = ' . ($G::hide_receive ? 'TRUE' : 'FALSE'), + ' hide_informational = ' . ($G::hide_informational ? 'TRUE' : 'FALSE'), + ' hide_all = ' . ($G::hide_all ? 'TRUE' : 'FALSE'), + " trans_fh_of = $G::trans_fh_of ($G::trans_fh_oh," . \*STDOUT . ')', + " trans_fh_ef = $G::trans_fh_ef ($G::trans_fh_eh," . \*STDERR . ')', + ]); + } + + if ($dump_args->{'TRANSPORT'} || $dump_args->{'ALL'}) { + push(@parts, [ + 'Transport Info:', + " type = $G::link{type}" + ]); + if ($G::link{type} eq 'socket-inet') { + push(@{$parts[-1]}, + ' inet protocol = ' . ($G::link{force_ipv4} ? '4' : ($G::link{force_ipv6} ? '6' : 'any')), + " server = $G::link{server}", + " port = $G::link{port}", + " local interface = $G::link{lint}", + " local port = $G::link{lport}", + ' copy routing = ' . ($opts->{copy_routing} ? $opts->{copy_routing} : 'FALSE'), + ); + } + elsif ($G::link{type} eq 'socket-unix') { + push(@{$parts[-1]}, " sockfile = $G::link{sockfile}"); + } + elsif ($G::link{type} eq 'pipe') { + push(@{$parts[-1]}, " process = $G::link{process}"); + } + else { + push(@{$parts[-1]}, " UNKNOWN TRANSPORT TYPE"); + } + } + + if ($dump_args->{'PROTOCOL'} || $dump_args->{'ALL'}) { + push(@parts, [ + 'Protocol Info:', + " protocol = $G::protocol", + " helo = $opts->{helo}", + " from = $opts->{from}", + " to = $opts->{to}", + ' force getpwuid = ' . ($opts->{force_getpwuid} ? 'TRUE' : 'FALSE'), + " quit after = $G::quit_after", + " drop after = $G::drop_after", + " drop after send = $G::drop_after_send", + ' server_only = ' . ($G::server_only ? 'TRUE' : 'FALSE'), + " timeout = $G::link{timeout}", + ' pipeline = ' . ($G::pipeline ? 'TRUE' : 'FALSE'), + ' prdr = ' . ($G::prdr ? 'TRUE' : 'FALSE'), + ]); + } + + if ($dump_args->{'XCLIENT'} || $dump_args->{'ALL'}) { + push(@parts, ['XCLIENT Info:']); + if ($G::xclient{try}) { + if ($G::xclient{optional} == 2) { push(@{$parts[-1]}, ' xclient = optional-strict'); } + elsif ($G::xclient{optional} == 1) { push(@{$parts[-1]}, ' xclient = optional'); } + else { push(@{$parts[-1]}, ' xclient = required'); } + push(@{$parts[-1]}, + ' no_verify = ' . ($G::xclient{no_verify} ? 'TRUE' : 'FALSE'), + ' before starttls = ' . ($G::xclient{before_tls} ? 'TRUE' : 'FALSE'), + ); + for (my $i = 0; $i < scalar(@{$G::xclient{strings}}); $i++) { + my $prefix = $i ? ' ' : ' strings ='; + push(@{$parts[-1]}, "$prefix XCLIENT $G::xclient{strings}[$i]"); + } + } else { + push(@{$parts[-1]}, ' xclient = no'); + } + } + + if ($dump_args->{'PROXY'} || $dump_args->{'ALL'}) { + push(@parts, ['PROXY Info:']); + if ($G::proxy{try}) { + push(@{$parts[-1]}, ' proxy = yes'); + push(@{$parts[-1]}, " version = $G::proxy{version}"); + if ($G::proxy{raw}) { + push(@{$parts[-1]}, " raw string = $G::proxy{raw}"); + } else { + push(@{$parts[-1]}, + ' family = ' . $G::proxy{attr}{family}, + ' source = ' . $G::proxy{attr}{source}, + ' source port = ' . $G::proxy{attr}{source_port}, + ' dest = ' . $G::proxy{attr}{dest}, + ' dest port = ' . $G::proxy{attr}{dest_port}, + ' protocol = ' . $G::proxy{attr}{protocol}, + ' command = ' . $G::proxy{attr}{command}, + ); + } + } else { + push(@{$parts[-1]}, ' proxy = no'); + } + } + + if ($dump_args->{'TLS'} || $dump_args->{'ALL'}) { + push(@parts, ['TLS / Encryption Info:']); + if ($G::tls || $G::tls_on_connect) { + if ($G::tls) { + if ($G::tls_optional == 2) { push(@{$parts[-1]}, ' tls = starttls (optional-strict)'); } + elsif ($G::tls_optional == 1) { push(@{$parts[-1]}, ' tls = starttls (optional)'); } + else { push(@{$parts[-1]}, ' tls = starttls (required)'); } + } + elsif ($G::tls_on_connect) { push(@{$parts[-1]}, ' tls = starttls on connect (required)'); } + push(@{$parts[-1]}, + " peer cert = $G::tls_get_peer_cert", + " local cert = $G::tls_cert", + " local key = $G::tls_key", + " local cipher list = $G::tls_cipher", + " ca path = $G::tls_ca_path", + " sni string = $G::tls_sni_hostname", + ' verify server cert = ' . ($G::tls_verify ? 'TRUE' : 'FALSE'), + ' available protocols = ' . join(', ', @G::tls_supported_protocols), + ' requested protocols = ' . join(', ', @G::tls_protocols), + ); + } + else { + push(@{$parts[-1]}, ' tls = no'); + } + } + + if ($dump_args->{'AUTH'} || $dump_args->{'ALL'}) { + push(@parts, ['Authentication Info:']); + if ($opts->{a_type}) { + if ($G::auth_optional == 2) { push(@{$parts[-1]}, ' auth = optional-strict'); } + elsif ($G::auth_optional == 1) { push(@{$parts[-1]}, ' auth = optional'); } + else { push(@{$parts[-1]}, ' auth = required'); } + push(@{$parts[-1]}, + " username = '$opts->{a_user}'", + " password = '%RAW_PASSWORD_STRING%'", + ' show plaintext = ' . ($G::auth_showpt ? 'TRUE' : 'FALSE'), + ' hide password = ' . ($G::auth_hidepw ? $G::auth_hidepw : 'FALSE'), + ' allowed types = ' . join(', ', @{$opts->{a_type}}), + ' extras = ' . join(', ', map { "$_=$G::auth_extras{$_}" } (sort(keys((%G::auth_extras))))), + ' type map = ' . join("\n".' 'x19, map { "$_ = ". join(', ', @{$G::auth_map_t{$_}}) } (sort(keys(%G::auth_map_t)))), + ); + } + else { + push(@{$parts[-1]}, " auth = no"); + } + } + + if (($dump_args->{'DATA'} || $dump_args->{'ALL'}) && !$skip->{'DATA'}) { + push(@parts, [ + 'DATA Info:', + ' data = <<.', + $opts->{data} + ]); + } + + # rejoin the parts into a string now + # this whole exercise was to avoid extra newlines when only dumping certain parts + foreach my $part (@parts) { + $part = join("\n", @$part) . "\n"; + } + return(join("\n", @parts)); } sub get_username { - my $force_getpwuid = shift; - if ($^O eq 'MSWin32') { - require Win32; - return Win32::LoginName(); - } - if ($force_getpwuid) { - return (getpwuid($<))[0]; - } - return getlogin() || (getpwuid($<))[0]; + my $force_getpwuid = shift; + if ($^O eq 'MSWin32') { + require Win32; + return Win32::LoginName(); + } + if ($force_getpwuid) { + return (getpwuid($<))[0]; + } + return $ENV{LOGNAME} || (getpwuid($<))[0]; } sub get_date_string { - return($G::date_string) if (length($G::date_string) > 0); + return($G::date_string) if (length($G::date_string) > 0); - my @l = localtime(); - my $o = 0; + my $et = time(); - if (!avail("date_manip")) { - ptrans(12, avail_str("date_manip").". Date strings will be in GMT"); - @l = gmtime(); - } else { - my @g = gmtime(); - $o = (timelocal(@l) - timelocal(@g))/36; - } - $G::date_string = sprintf("%s, %02d %s %d %02d:%02d:%02d %+05d", - (qw(Sun Mon Tue Wed Thu Fri Sat))[$l[6]], - $l[3], - (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$l[4]], - $l[5]+1900, $l[2], $l[1], $l[0], - $o - ); + if (!avail("date_manip")) { + ptrans(12, avail_str("date_manip").". Date strings will be in GMT"); + my @l = gmtime($et); + $G::date_string = sprintf("%s, %02d %s %d %02d:%02d:%02d %+05d", + (qw(Sun Mon Tue Wed Thu Fri Sat))[$l[6]], + $l[3], + (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$l[4]], + $l[5]+1900, $l[2], $l[1], $l[0], + 0); + } else { + $G::date_string = POSIX::strftime("%a, %d %b %Y %H:%M:%S %z", localtime($et)); + } + return($G::date_string); } # partially Cribbed from "Programming Perl" and MIME::Base64 v2.12 sub db64 { - my $s = shift; - if (load("MIME::Base64")) { - return(decode_base64($s)); - } else { - $s =~ tr#A-Za-z0-9+/##cd; - $s =~ s|=+$||; - $s =~ tr#A-Za-z0-9+/# -_#; - my $r = ''; - while ($s =~ s/(.{1,60})//s) { - $r .= unpack("u", chr(32 + int(length($1)*3/4)) . $1); - } - return($r); - } + my $s = shift; + if (load("MIME::Base64")) { + return(decode_base64($s)); + } else { + $s =~ tr#A-Za-z0-9+/##cd; + $s =~ s|=+$||; + $s =~ tr#A-Za-z0-9+/# -_#; + my $r = ''; + while ($s =~ s/(.{1,60})//s) { + $r .= unpack("u", chr(32 + int(length($1)*3/4)) . $1); + } + return($r); + } } # partially Cribbed from MIME::Base64 v2.12 sub eb64 { - my $s = shift; - my $e = shift || ''; # line ending to use "empty by default" - if (load("MIME::Base64")) { - return(encode_base64($s, $e)); - } else { - my $l = length($s); - chomp($s = pack("u", $s)); - $s =~ s|\n.||gms; - $s =~ s|\A.||gms; - $s =~ tr#` -_#AA-Za-z0-9+/#; - my $p = (3 - $l%3) % 3; - $s =~ s/.{$p}$/'=' x $p/e if ($p); - $s =~ s/(.{1,76})/$1$e/g if (length($e)); - return($s); - } + my $s = shift; + my $e = shift || ''; # line ending to use "empty by default" + if (load("MIME::Base64")) { + return(encode_base64($s, $e)); + } else { + my $l = length($s); + chomp($s = pack("u", $s)); + $s =~ s|\n.||gms; + $s =~ s|\A.||gms; + $s =~ tr#` -_#AA-Za-z0-9+/#; + my $p = (3 - $l%3) % 3; + $s =~ s/.{$p}$/'=' x $p/e if ($p); + $s =~ s/(.{1,76})/$1$e/g if (length($e)); + return($s); + } +} + +sub build_version { + my $static = shift; + my $svn = shift; + + if ($static ne 'DEVRELEASE') { + # if gen-util passed in a static version, use it unconditionally + return $static; + } elsif ($svn =~ /\$Id:\s+\S+\s+(\d+)\s+(\d+)-(\d+)-(\d+)\s+/) { + # otherwise, this is a dev copy, dynamically build a version string for it + return("$2$3$4.$1-dev"); + } else { + # we wanted a dynamic version, but the SVN Id tag wasn't in the format + # we expected, punt + return("DEVRELEASE"); + } } sub ext_usage { - if ($ARGV[0] =~ /^--help$/i) { - require Config; - $ENV{PATH} .= ":" unless $ENV{PATH} eq ""; - $ENV{PATH} = "$ENV{PATH}$Config::Config{'installscript'}"; - $< = $> = 1 if ($> == 0 || $< == 0); - exec("perldoc", $0) || exit(1); - # make parser happy - %Config::Config = (); - } elsif ($ARGV[0] =~ /^--version$/i) { - print "$p_name version $p_version\n\n$p_cp\n"; - } else { - return; - } + require Config; + $ENV{PATH} .= ":" unless $ENV{PATH} eq ""; + $ENV{PATH} = $ENV{PATH} . $Config::Config{'installscript'}; + $< = $> = 1 if ($> == 0 || $< == 0); + exec("perldoc", $0) || exit(1); + # make parser happy + %Config::Config = (); - exit(0); + exit(0); } -__END__ - -=head1 NAME - -swaks - SMTP transaction tester - -=head1 USAGE - -swaks [--help|--version] | (see description of options below) - -=head1 OPTIONS - -=over 4 - -=item --pipe - -This option takes as its argument a program and the program's arguments. If this option is present, swaks opens a pipe to the program and enters an SMTP transaction over that pipe rather than connecting to a remote server. Some MTAs have testing modes using stdin/stdout. This option allows you to tie into those options. For example, if you implemented DNSBL checking with exim and you wanted to make sure it was working, you could run 'swaks --pipe "exim -bh 127.0.0.2"'. - -In an ideal world the process you are talking to should behave exactly like an SMTP server on stdin and stdout. Any debugging should be sent to stderr, which will be directed to your terminal. In the real world swaks can generally handle some debug on the child's stdout, but there are no guarantees on how much it can handle. - -=item --socket - -This option takes as its argument a unix domain socket file. If this option is present, swaks enters an SMTP transaction over over the unix domains socket rather than over an internet domain socket. I think this option has uses when combined with a (yet unwritten) LMTP mode, but to be honest at this point I just implemented it because I could. - -=item -l, --input-file - -Argument to -l must be a path to a file containing TOKEN->VALUE pairs. The TOKEN and VALUE must be separated by whitespace. These tokens set values which would otherwise be set by command line arguments. See the description of the corresponding command line argument for details of each token. Valid tokens are FROM (-f), TO (-t), SERVER (-s), DATA (-d), HELO (-h), PORT (-p), INTERFACE (-li), and TIMEOUT (-to). - -=item -t, --to - -Use argument as "RCPT TO" address, or prompt user if no argument specified. Overridden by -l token TO. Multiple recipients can be specified by supplying as one comma-delimited argument. - -There is no default for this option. If no to addess is specified with -t or TO token, user will be prompted for To: address on STDIN. - -=item -f, --from - -Use argument as "MAIL FROM" address, or prompt user if no argument specified. Overridden by -l token FROM. If no from address is specified, default is user@host, where user is the best guess at user currently running program, and host is best guess at DNS hostname of local host. The string <> can be supplied to mean the null sender. - -=item -s, --server - -Use argument as mail server to which to connect, or prompt user if no argument specified. Overridden by -l token SERVER. If unspecified, swaks tries to determine primary MX of destination address. If Net::DNS module is not available, tries to connect to A record for recipient's domain. - -=item -p, --port - -Use argument as port to connect to on server, or prompt user if no argument is specified. This can be either a port number or a service name. Overridden by -l token PORT. If unspecified, swaks will use service lmtp if --protocol is LMTP, service smtps if --tls-on-connect is used, and smtp otherwise. - -=item -h, --helo, --ehlo - -Use argument as argument to SMTP EHLO/HELO command, or prompt use if no argument is specified. Overridden by -l token HELO. If unspecified, swaks uses best guess at DNS hostname of local host. - -=item -d, --data - -Use argument as DATA portion of SMTP transaction, or prompt user if no argument specified. Overridden by -l token DATA. - -This string should be on one single line, with a literal \n representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. Very basic token parsing is done. %F is replaced with the value that will be used for "MAIL FROM", %T is replaced with "RCPT TO" values, %D is replaced with a timestamp, %H is replaced with the contents of --add-header, and %B is replaced with the message body. See the --body option for the default body text. - -Default value for this option is "Date: %D\nTo: %T\nFrom: %F\nSubject: test %D\nX-Mailer: swaks v$p_version jetmore.org/john/code/#swaks\n%H\n\n%B\n". - -=item --body - -Specify the body of the email. The default is "This is a test mailing". If no argument to --body, you will be prompted to supply one. If '-' is supplied, body will be read from standard input. If any other text is provided and the text represents an openable file, the content of that file is used as the body. If it does not respresent an openable file, the text itself is used as the body. - -=item --attach - -When one or more --attach option is supplied, the message is changed into a multipart/mixed MIME message. The arguments to --attach are processed the same as --body with regard to stdin, file contents, etc. --attach can be supplie multiple times to create multiple attachments. By default each attachment is attached as a application/octet-stream file. See --attach-type for changing this behaviour. - -When the message changes to MIME format, the previous body (%B) is attached as a text/plain type as the first attachment. --body can still be used to specify the contents of this body attachment. - -It is legal for '-' (STDIN) to be specified as an argument multiple times (once for --body and multiple times for --attach). In this case, the same content will be attached each time it is specified. This is useful for attaching the same content with multiple MIME types. - -=item --attach-type - -By default, content that gets MIME attached to a message with the --attach option is encoded as application/octet-stream. --attach-type changes the mime type for every --attach option which follows it. It can be specified multiple times. - -=item -ah, --add-header - -In the strictest sense, all this does is provide a value that replaces the %H token in the data. Because of where %H is located in the default DATA, practically it is used to add custom headers without having to recraft the entire body. - -The option can either be specified multiple times or a single time with multiple headers seperated by a literal '\n' string. So, "--add-header 'Foo: bar' --add-header 'Baz: foo'" and "--add-header 'Foo: bar\nBaz: foo'" end up adding the same two headers. - -=item --header, --h-Header - -These options allow a way to change headers that already exist in the DATA. These two calls do the same thing: - ---header "Subject: foo" ---h-Subject foo - -Subject is the example used. If the header does not exist in the body already, these calls behave identically to --add-header. The purpose of this option it to provide a fast way to change the nature of the default DATA for specific tests. For instance if you wanted to test a subject filer in a mail system, you could use --h-Subject "SPAM STRING" to test rather than having to craft and entire new DATA string to pass to --data. - -=item --timeout - -Use argument as the SMTP transaction timeout, or prompt user if no argument given. Overridden by the -l token TIMEOUT. Argument can either be a pure digit, which will be interpretted as seconds, or can have a specifier s or m (5s = 5 seconds, 3m = 180 seconds). As a special case, 0 means don't timeout the transactions. Default value is 30s. - -=item --protocol - -Specify which protocol to use in the transaction. Valid options are shown in the table below. Currently the 'core' protocols are SMTP, ESMTP, and LMTP. By using variations of these protocol types one can specify default ports, whether authentication should be attempted, and the type of TLS connection that should be attempted. The default protocol is ESMTP. This table demonstrates the available arguments to --protocol and the options each sets as a side effect: - - HELO AUTH TLS PORT - -------------------------------------------------- - SMTP HELO smtp / 25 - SSMTP EHLO->HELO -tlsc smtps / 465 - SSMTPA EHLO->HELO -a -tlsc smtps / 465 - SMTPS HELO -tlsc smtps / 465 - ESMTP EHLO->HELO smtp / 25 - ESMTPA EHLO->HELO -a smtp / 25 - ESMTPS EHLO->HELO -tls smtp / 25 - ESMTPSA EHLO->HELO -a -tls smtp / 25 - LMTP LHLO lmtp / 24 - LMTPA LHLO -a lmtp / 24 - LMTPS LHLO -tls lmtp / 24 - LMTPSA LHLO -a -tls lmtp / 24 - -=item -li, --local-interface - -Use argument as the local interface for the SMTP connection, or prompt user if no argument given. Overridden by the -l token INTERFACE. Argument can be an IP or a hostname. Default action is to let OS choose local interface. - -=item -g - -If specified, swaks will read the DATA value for the mail from STDIN. If there is a From_ line in the email, it will be removed (but see -nsf option). Useful for delivering real message (stored in files) instead of using example messages. - -=item -nsf, --no-strip-from - -Don't strip the From_ line from the DATA portion, if present. - -=item -n, --suppress-data - -If this option is specified, swaks summarizes the DATA portion of the SMTP transaction instead of printing every line. - -=item -q, --quit-after - -The argument to this option is used as an indicator of where to quit the SMTP transaction. It can be thought of as "quit after", with valid arguments CONNECT, FISRT-HELO, TLS, HELO, AUTH, MAIL, and RCPT. In a non-STARTTLS session, FIRST-HELO and HELO behave the same way. In a STARTTLS session, FIRST-HELO quits after the first HELO sent, while HELO quits after the second HELO is sent. - -For convenience, LHLO and EHLO are synonyms for HELO, FIRST-EHLO and FIRST-LHLO for FIRST-HELO, FROM for MAIL, and TO for RCPT. - -=item -m - -Emulate Mail command. Least used option in swaks. - -=item --support - -Cause swaks to print its capabilities and exit. Certain features require non-standard perl modules. This options evaluates whether these modules are present and lets you know which functionality is present. - -=item -S, --silent - -Cause swaks to be silent. "-S" causes swaks to print no output until an error occurs, after which all output is shown. "-S -S" causes swaks to only show error conditions. "-S -S -S" shows no output. - -=item --pipeline - -If the remote server supports it, attempt SMTP PIPELINING (RFC 2920). This is a younger option, if you experience problems with it please notify the author. Potential problem areas include servers accepting DATA even though there were no valid recipients (swaks should send empty body in that case, not QUIT) and deadlocks caused by sending packets outside the tcp window size. - -=item -tls - -Require connection to use STARTTLS. Exit if TLS not available for any reason (not advertised, negotiations failed, etc). - -=item -tlso, --tls-optional - -Attempt to use STARTTLS if possible, continue t/ normal transaction if TLS unavailable. - -=item -tlsc, --tls-on-connect - -Initiate a TLS connection immediately on connection. Use to test smtps/ssmtp servers. If this options is specified, the default port changes from 25 to 465, though this can still be overridden with the -p option. - -=item -a, --auth - -Require authentication. If Authentication fails or is unavailable, stop transaction. -a can take an argument specifying which type(s) of authentication to try. If multiple, comma-delimited arguments are given, each specified auth type is tried in order until one succeeds or they all fail. swaks currently supports PLAIN, LOGIN, and CRAM-MD5. If no argument is given any available authentication type is used. If neither password (-ap) or username (-au) is supplied on command line, swaks will prompt on STDIN. - -SPA (NTLM/MSN) authentication is now supported. Tested as a client against Exim and Stalker's CommuniGate, but implementation may be incomplete. Authen::NTLM is currently required. Note that CPAN hosts two different Authen::NTLM modules. Current implementation requires Mark Bush's implementation (Authen/NTLM-1.02.tar.gz). Plan to reimplement directly at some point to avoid confusion. - -DIGEST-MD5 is now supported. Tested as a client only against Stalker's Communigate, so implementation may be incomplete. Requires Authen::DigestMD5 module. - -CRAM-SHA1 is now supported. Only tested against a hacked server implementation in Exim, so may be incomplete or incorrect. Requires Digest::SHA1 module. - -=item -ao, --auth-optional - -Same as -a, but if authentication is unavailable or fails, attempts to continue transaction. - -=item -au, --auth-user - -Supply the username for authentication. The string <> can be supplied to mean an empty username. - -For SPA authentication, a "domain" can be specified after the regular username with a % seperator. For instance, if "-ap user@example.com%NTDOM" is passed, "user@example.com" is the username and "NTDOM" is the domain. NOTE: I don't actually have access to a mail server where the domain isn't ignored, so this may be implemented incorrectly. - -=item -ap, --auth-password - -Supply the password for authentication. The string <> can be supplied to mean an empty password. - -=item -am --auth-map - -Provides a way to map alternate names onto base authentication types. Useful for any sites that use alternate names for common types. This functionality is actually used internally to map types SPA and MSN onto the base type NTLM. The command line argument to simulate this would be "--auth-map SPA=NTLM,MSN=NTLM". The base types supported are LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5, and NTLM. SPA and MSN are mapped on to NTLM automatically. - -=item -apt, --auth-plaintext - -Instead of showing AUTH strings literally (in base64), translate them to plaintext before printing on screen. - -=item -nth, --no-hints - -Don't show transaction hints. (Useful in conjunction with -hr to create copy/paste-able transactions - -=item -hr, --hide-receive - -Don't display reception lines - -=item -hs, --hide-send - -Don't display sending lines - -=item -stl, --show-time-lapse - -Display time lapse between send/receive pairs. If 'i' is provided as argument or the Time::HiRes module is unavailable the time lapse will be integer only, otherwise it will be to the thousandth of a second. - -=item --force-getpwuid - -In releases 20050709.1 and earlier of swaks the local_part of an automatically generated sender email address would be found using the getpwuid system call on the euid of the current process. Depending on the users' desires, this may be confusing. Following the 20050709.1 release the local_part is not looked up via the getlogin() funtion which attempts to look up the actual username of the logged in user, regardless of the euid of the process they are currently running. - -An example of where this might be an issue is running swaks under sudo for testing reasons when interacting with --pipe or --socket. It makes sense that you need to run the process as a specific username but you would prefer your email to be from your real username. You could always do this manually using the -s option, but this is an attempt to make it easier. - ---force-getpwuid forces the old behaviour for anyone who prefered that behaviour. Also, if there is no "real" user for getlogin() to look up, the old getpwuid method will be used. This would happen if the process was run from cron or some other headless daemon. - -=item --help - -This screen. - -=item --version - -Version info. - -=back - -=head1 EXAMPLES - -=over 4 - -=item swaks - -prompt user for to address and send a default email. - -=item cat mailfile | swaks -g -n -t user@example.com -tlso -a -au user -ap password - -send the contents of "mailfile" to user@example.com, using TLS if available, requiring authentication, using user/password as authentication information. - -=back - -=head1 COMMENTS - -This program was written because I was testing a new MTA on an alternate port. I did so much testing that using interactive telnet grew tiresome. Over the next several years this program was fleshed out and every single option was added as a direct need of some testing I was doing as the mail admin of a medium sized ISP, with the exception of TLS support which was added on a whim. As such, all options are reasonably well thought out and fairly well tested (though TLS could use more testing). - -=head1 REQUIRES - -swaks does not have any single requirement except the standard module Getopt::Long. However, there may be modules that are required for a given invocation of swaks. The following list details the features reported by the --support option, what is actually being tested, and the consequences of the feature being reported as "not available" - -=over 4 - -=item AUTH CRAM-MD5 - -CRAM-MD5 authentication requires the Digest::MD5 perl module. If this is unavailable and authentication is required, swaks will error if CRAM-MD5 was the specific authentication type requested, or if no specific auth type was requested but CRAM-MD5 was the only type advertised by the server. - -=item AUTH CRAM-SHA1 - -CRAM-SHA1 authentication requires the Digest::SHA1 perl module. If this is unavailable and authentication is required, swaks will error if CRAM-SHA1 was the specific authentication type requested, or if no specific auth type was requested but CRAM-SHA1 was the only type advertised by the server. - -=item AUTH DIGEST-MD5 - -DIGEST-MD5 authentication requires the Authen::DigestMD5 perl module. If this is unavailable and authentication is required, swaks will error if DIGEST-MD5 was the specific authentication type requested, or if no specific auth type was requested but DIGEST-MD5 was the only type advertised by the server. - -=item AUTH NTLM - -NTLM/SPA/MSN authentication requires the Authen::NTLM perl module. If this is unavailable and authentication is required, swaks will error if NTLM was the specific authentication type requested, or if no specific auth type was requested but NTLM was the only type advertised by the server. Note that there are two modules using the Authen::NTLM namespace on CPAN. The Mark Bush implementation (Authen/NTLM-1.02.tar.gz) is the version required here. - -=item Basic AUTH - -All authentication types require base64 encoding and decoding. If possible, swaks uses the MIME::Base64 perl module to perform these actions. However, if MIME::Base64 is not available swaks will use its own onboard base64 routines. These are slower than the MIME::Base64 routines and less reviewed, though they have been tested thoroughly. When possible it is recommended that you install MIME::Base64. - -=item Date Manipulation - -swaks generates an RFC compliant date string when it interpolates the %D token in message bodies. In order to build the GMT offset in this string, it needs the Time::Local module. It would be very odd for this module not to be available because it has been included in the perl distribution for some time. However, if it is not loadable for some reason and swaks interpolates a %D token (as it would when using the default body), the date string is in GMT instead of your local timezone. - -=item High Resolution Timing - -When diagnosing SMTP delays using --show-time-lapse, by default high resolution timing is attempted using the Time::HiRes module. If this module is not available, swaks uses a much poorer timing source with one second granularity. - -=item Local Hostname Detection - -swaks uses your local machine's hostname to build the HELO string and sending email address when they are not specified on the command line. If the Sys::Hostname module (which is a part of the base distribution) is not available for some reason, the user is prompted interactively for the HELO and sender strings. Note that Sys::Hostname can sometimes fail to find the local hostname even when the module is available, which has the same behaviour. - -=item MX Routing - -If the destination mail server is not specified using the --server option, swaks attempts to use DNS to route the message based on the recipient email address. If the Net::DNS perl module is not available, swaks uses 'localhost' as the outbound mail server. - -=item Pipe Transport - -The IPC::Open2 module is required to deliver a message to a spawned subprocess using the --pipe option. If this module, which is included in the base perl distribution, in not available, attempting to call swaks with the --pipe option will result in an error. - -=item Socket Transport - -The IO::Socket module is required to deliver a message to an internet domain socket (the default behaviour of swaks) and to a unix domain socket (specified with the --socket option). If this module, which is included in the base perl distribution, is not available, attempting to call swaks with the --server or --socket options (or none of the --socket, --server, and --pipe options) will result in an error. - -=item TLS - -TLS functionality requires the Net::SSLeay perl module. If this module is not available and TLS was required (using the --tls-on-connect or --tls options), the session will error out. If TLS was requested but not required (using the --tls-optional option), swaks will continue but not attempt a TLS session. - -=back - -=head1 PORTABILITY - -=over 4 - -=item Operating Systems - -This program was primarily intended for use on unix-like operating systems, and it should work on any reasonable version thereof. It has been developed and tested on Solaris, Linux, and Mac OS X and is feature complete on all of these. - -This program is known to demonstrate basic functionality on Windows using ActiveState's Perl. It has not been fully tested. Known to work are basic SMTP functionality and the LOGIN, PLAIN, and CRAM-MD5 auth types. Unknown is any TLS functionality and the NTLM/SPA and Digest-MD5 auth types. - -Because this program should work anywhere Perl works, I would appreciate knowing about any new operating systems you've thoroughly used swaks on as well as any problems encountered on a new OS. - -=item Mail Servers - -This program was almost exclusively developed against Exim mail servers. It was been used casually by the author, though not thoroughly tested, with sendmail, smail, and Communigate. Because all functionality in swaks is based off of known standards it should work with any fairly modern mail server. If a problem is found, please alert the author at the address below. - -=back - -=head1 EXIT CODES - -=over 4 - -=item 0 - -no errors occurred - -=item 1 - -error parsing command line options - -=item 2 - -error connecting to remote server - -=item 3 - -unknown connection type - -=item 4 - -while running with connection type of "pipe", fatal problem writing to or reading from the child process - -=item 5 - -while running with connection type of "pipe", child process died unexpectedly. This can mean that the program specified with --pipe doesn't exist. - -=item 6 - -Connection closed unexpectedly. If the close is detected in response to the 'QUIT' swaks sends following an unexpected response, the error code for that unexpected response is used instead. - -For instance, if a mail server returns a 550 response to a MAIL FROM: and then immediately closes the connection, swaks detects that the connection is closed, but uses the more specific exit code 23 to detail the nature of the failure. - -If instead the server return a 250 code and then immediately closes the connection, swaks will use the exit code 6 because there is not a more specific exit code. - -=item 10 - -error in prerequisites (needed module not available) - -=item 21 - -error reading initial banner from server - -=item 22 - -error in HELO transaction - -=item 23 - -error in MAIL transaction - -=item 24 - -no RCPTs accepted - -=item 25 - -server returned error to DATA request - -=item 26 - -server did not accept mail following data - -=item 27 - -server returned error after normal-session quit request - -=item 28 - -error in AUTH transaction - -=item 29 - -error in TLS transaction - -=item 32 - -error in EHLO following TLS negotiation - -=back - -=head1 CONTACT - -=over 4 - -=item proj-swaks@jetmore.net - -Please use this address for general contact, questions, patches, requests, etc. - -=item updates-swaks@jetmore.net - -If you would like to be put on a list to receive notifications when a new version of swaks is released, please send an email to this address. - -=item jetmore.org/john/code/#swaks - -Change logs, this help, and the latest version is found at this link. - -=back - -=cut \ No newline at end of file diff --git a/tmsu b/tmsu deleted file mode 100755 index d101c87..0000000 Binary files a/tmsu and /dev/null differ diff --git a/whoisplaying b/whoisplaying index 99e7be2..9617931 100755 --- a/whoisplaying +++ b/whoisplaying @@ -1,4 +1,5 @@ #!/bin/sh +# The output for this will likely be pulseaudio for i in /proc/[0-9]*/fd/* do var="$(readlink $i)" @@ -6,4 +7,4 @@ do then echo $i fi -done \ No newline at end of file +done diff --git a/wm_minimize.sh b/wm_minimize.sh deleted file mode 100755 index 7565110..0000000 --- a/wm_minimize.sh +++ /dev/null @@ -1,40 +0,0 @@ -#! /bin/sh - -# minimize/restore windows on current desktop -# ----------------------------------- -# vermaden [AT] interia [DOT] pl -# http://toya.net.pl/~vermaden/links.htm - -CURRENT_DESKTOP=$( wmctrl -d | egrep "^[0-9][ ]{2}\*" | awk '{print $1}' ) -WINDOW_LIST=$( wmctrl -l | egrep "^[0-9]x.{8}\ {2}${CURRENT_DESKTOP}" | awk '{print $1}' ) - -WINDOW_COUNT=0 -for WINDOW in ${WINDOW_LIST} ;do - WINDOW_COUNT=$(( ${WINDOW_COUNT} + 1 )) -done - -minimize () { - for WINDOW in ${WINDOW_LIST}; do - wmctrl -t ${CURRENT_DESKTOP} -i -r ${WINDOW} -b add,hidden - done - } - -restore () { - for WINDOW in ${WINDOW_LIST}; do - wmctrl -t ${CURRENT_DESKTOP} -i -r ${WINDOW} -b remove,hidden - done - } - -MINIMIZED=0 -for WINDOW in ${WINDOW_LIST}; do - if xprop -id ${WINDOW} _NET_WM_STATE | grep -q NET_WM_STATE_HIDDEN; then - MINIMIZED=$(( ${MINIMIZED} + 1 )) - fi -done - -if [ ${MINIMIZED} -eq ${WINDOW_COUNT} ]; then - restore -else - minimize -fi - diff --git a/youtube-dl b/youtube-dl deleted file mode 100755 index 5fb8363..0000000 Binary files a/youtube-dl and /dev/null differ