diff --git a/Changes b/Changes index e61eae37..16c53b1d 100644 --- a/Changes +++ b/Changes @@ -851,3 +851,4 @@ * 20190817 Remove re-prompting for port when an invalid service name was supplied. Just error and exit instead * 20191005 Fixed typos in base.pod and recipes.pod +> 20191006 released 20190914.0 diff --git a/README.md b/README.md new file mode 100644 index 00000000..54295b9b --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Swaks - Swiss Army Knife for SMTP + +Swaks is a featureful, flexible, scriptable, transaction-oriented SMTP test tool written and maintained by [John Jetmore](https://jetmore.org/john/). It is free to use and licensed under the GNU GPLv2. Features include: + +* SMTP extensions including TLS, authentication, pipelining, PROXY, PRDR, and XCLIENT +* Protocols including SMTP, ESMTP, and LMTP +* Transports including UNIX-domain sockets, internet-domain sockets (IPv4 and IPv6), and pipes to spawned processes +* Completely scriptable configuration, with option specification via environment variables, configuration files, and command line + +The official project page is https://jetmore.org/john/code/swaks/. + +## Download + +The latest version of Swaks is **20190914.0**, which can be downloaded as a [package](https://jetmore.org/john/code/swaks/files/swaks-20190914.0.tar.gz) or a [standalone script](https://jetmore.org/john/code/swaks/files/swaks-20190914.0/swaks). + +There is also a [versions page](https://jetmore.org/john/code/swaks/versions.html) which lists every released version of Swaks, complete with changelogs and download links. + +## Documentation + +The [reference documentation](https://jetmore.org/john/code/swaks/latest/doc/ref.txt) from the latest release, which includes quick-start examples, is available. There is also an [Occasionally Asked Questions](https://jetmore.org/john/code/swaks/faq.html) document. + +## Communications + +Feedback and meaningful questions about how to use Swaks are welcome. However, since Swaks is only maintained by a single person as a hobby, there is no guarantee of a timely response. + +### Release Notification + +* [Send a mail](mailto:updates-swaks@jetmore.net). You will receive notifications of new releases via email. No other email will ever be sent to this list. +* [Follow @SwaksSMTP](https://twitter.com/SwaksSMTP) on twitter. Very rarely contains non-release content. +* [Blog](https://www.jetmore.org/john/blog/c/swaks/). Swaks-specific blog category (RSS available). Very rarely contains non-release content. + +### Help and Feedback + +* [Issues](https://github.com/jetmore/swaks/issues) - Open an issue for feature requests and bugs. +* [Contact the author](mailto:proj-swaks@jetmore.net) - suggestion, tips, patches, feedback, critiques always welcome. + +## License + +[GNU GPLv2](https://choosealicense.com/licenses/gpl-2.0/) diff --git a/RELEASE/README.txt b/RELEASE/README.txt index 7300dd8a..1c3e97a7 100644 --- a/RELEASE/README.txt +++ b/RELEASE/README.txt @@ -39,19 +39,24 @@ Check the following files doc/recipes.txt Hints, tips, tricks that don't fit in the reference +------------------------------ +Source +------------------------------ +The Swaks source code is available at https://github.com/jetmore/swaks + ------------------------------ Communication ------------------------------ -The main Swaks website is currently http://jetmore.org/john/code/swaks/ Ways to stay up to date on new releases: - 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 - Help: send questions to proj-swaks@jetmore.net + Homepage: https://jetmore.org/john/code/swaks/ + Online Docs: https://jetmore.org/john/code/swaks/latest/doc/ref.txt + https://jetmore.org/john/code/swaks/faq.html + Announce List: send mail to updates-swaks@jetmore.net + Project RSS: https://jetmore.org/john/blog/c/swaks/feed/ + Twitter: https://www.twitter.com/SwaksSMTP + Help: send questions to proj-swaks@jetmore.net +Bugs / Feature Requests: https://github.com/jetmore/swaks/issues ------------------------------ Authorship @@ -81,27 +86,34 @@ A full copy of this license should be available in the LICENSE.txt file. ------------------------------ Change Summary ------------------------------ -v20181104.0 +v20190914.0 New Features: - * Added --dump-mail option. - * Added --xclient-delim, --xclient-destaddr, --xclient-destport, - --xclient-no-verify, and --xclient-before-starttls options. + * Source is now available on github.com/jetmore/swaks + * Added --body-attach option to allow more granularity in setting body + information + * Added 'data' and 'dot' as valid --drop-after-send and + --drop-after arguments + * Added %NEWLINE% as a new --data token Notable Changes: - * XCLIENT can now send multiple XCLIENT requests. Because of this, - --xclient and --xclient-ATTR values are no longer merged into one - string. This breaks previously documented behavior. - * Numerous improvements to the output of --dump and --dump-as-body, - including the ability to limit output by section, layout improvements, - adding missing options to output, and fixing bugs. + * Options provided via environment variable are now sorted before + processing to provide a deterministic processing order + * Option bundling is no longer enabled. This fixes several option + processing oddities, like "-foobar" being interpreted as + "-f oobar" + * If the arg to --data looks like a file but is not openable, error + and exit instead of using it the file name as the raw data value + * Remove interactive prompts for --helo and --from when hostname cannot + be determined internally, just error and exit instead. If the user + was not expecting an interactive experience, don't start one + * Remove re-prompting for port when an invalid service name was supplied, + just error and exit instead. If the user was not expecting an + interactive experience, don't start one Notable Bugs Fixed: - * Fixed bug preventing Proxy from working with --tls-on-connect. - * XCLIENT is now sent after STARTTLS to match with Postfix's expectations. - * Fixed bug which could allow mail sending to proceed without a valid - recipient. - * Replacing a multi-line header via --header or --h-HEADER now replaces - the entire header, not just the first line. - * The option for specifying the local port was documented as --local-port - but implemented as --lport. Both are now documented and implemented. - * Fixed two bugs which prevented interactions between --dump, - --auth-hide-password, --dump-as-body, and --dump-as-body-shows-password - from producing consistent output. + * Handle malformed headers more gracefully in header replacement + * Fix bug causing the processing of options prefixed with the negating + "no-" to work unreliably + * --version and --help should work even if they aren't the very + first option + * -S is now a distinct option from -s, as documented + * Fix bug preventing the --option=arg option format from being + unusable with --header and --attach* options diff --git a/RELEASE/doc/Changes.txt b/RELEASE/doc/Changes.txt index 79c588b3..755cc72c 100644 --- a/RELEASE/doc/Changes.txt +++ b/RELEASE/doc/Changes.txt @@ -1,5 +1,78 @@ +> 20191006 released 20190914.0 +* 20191005 Fixed typos in base.pod and recipes.pod +* 20190817 Remove re-prompting for port when an invalid service name was + supplied. Just error and exit instead +* 20190817 Cleaning up error messages that contained extra newlines +* 20190817 Remove interactive prompts for helo and from when hostname + cannot be determined internally. Just error instead. +* 20190817 Rearrange internal option definition structure in preparation + for major rework +* 20180816 Rework how the --show-time-lapse option is tracked internally + and displayed in --dump output +* 20190815 --protocol's argument was incorrectly marked as optional +* 20190815 Updating copyright year to 2019 +* 20190814 --use-old-data-tokens was not completely removed, clean up +* 20190814 --tls-optional-strict was incorrectly marked internally as + optionally accepting an argument +* 20190713 Fix handling of --option=arg option format which prevented it + from being used with --header and --attach* options +* 20190713 --attach option processing was calling die() instead of + ptrans/exit on error +* 20190713 If the arg to --data looks like a file but is not openable, + error and exit instead of using it the file name as the raw + data value +* 20190713 Add %NEWLINE% as a new --data token +* 20190713 Small code tidy around %DATE% token replacement +* 20190712 Enforce key=value format for arguments to --auth-extra and + --auth-map +* 20190710 Clarify how XCLIENT arguments are grouped in --xclient doc +* 20190618 Typo in documentation for --ehlo, reported by Konstantin Stephan +* 20190216 Adding data and dot as valid --drop-after-send and --drop-after + arguments +* 20190105 Add documentation for missing --quit-after synonym STARTTLS +* 20190105 Update copyright year to 2019 +* 20181225 --copy-routing should error when no argument given. +* 20181225 Add validation to --proxy-family (when proxy-version=1) and + --proxy-version options. +* 20181210 Turn off option bundling. No practical use and it could cause + real confusion (with bundling turned on, -foobar was "-f oobar" + instead of an unknown option. +* 20181210 Turn on case-sensitivity for configuration options. Needed to + make -S distinct from -s, as documented. +* 20181210 Add a flag for --dump-mail in the OUTPUT section of --dump +* 20181210 --version and --help should work even if they aren't the very + first option. +* 20181208 When processing config file options with no leading '-' and any + environment variable config, prefix the option with '--' for + processing, not '-'. Bandaid for very minor difference between + '-' and '--' option processing which I hope to fix soon. +* 20181204 Adding an ENVIRONMENT VARIABLES section to the doc. +* 20181204 Tidying and clarifying the OPTION PROCESSING section of the docs. +* 20181203 Fix bug causing in "no-" option processing to work unreliably. +* 20181202 Document the unreliability of using environment variables to unset + other environment variable options with the "no-" prefix. +* 20181202 Document the general rule that when processing duplicate options, + the last option specified wins, both inter- and intra-method. +* 20181202 Since there is no inherent order to options provided in environment + variables, sort them before processing to define an order. +* 20181201 Config file fixes around searching default $SWAKS_HOME, $HOME, + and $LOGDIR locations: + - Searching default locations for the first existing + PATH/.swaksrc did not actually work as documented. + - If none of the default search environment variables was set, + Swaks would not process the "portable" defaults optionally + stored in the actual swaks script. +* 20181110 Implement --body-attach option to allow more granularity in + setting body information (different mime types, alternatives, etc). +* 20181110 Fix --attach* option processing to remove possibly ambiguity +* 20181110 Fix issue with malformed headers. Don't fall over if header + doesn't contain a colon or looks like an illegal continuation. +* 20181110 Doc fix for default body - %SWAKS_VERSION% missing trailing char. +* 20181110 --add-header documentation was still referencing a single-char, + no longer valid, replacement token. Replace with the correct token. > 20181104 released 20181104.0 * 20181104 Update release tooling after development environment changes +* 20181104 Rename LICENSE and README to have .txt extension * 20181104 Spelling fixes * 20181104 Clean up contact/notifications - remove Google+ everywhere, add RSS and Twitter to base.pod diff --git a/RELEASE/doc/recipes.pod b/RELEASE/doc/recipes.pod index 759eb317..48f33ed3 100644 --- a/RELEASE/doc/recipes.pod +++ b/RELEASE/doc/recipes.pod @@ -1,8 +1,8 @@ =pod -=head1 swaks - Recipes for use +=head1 Swaks - Recipes for use -Because swaks is a very flexible tool it can be hard to fully grasp what it can do (and few people want or need to). However, I have used swaks in interesting ways, and others have done the same and shared them with me. I hate for these interesting ideas go to waste, so I am including them in this file. +Because Swaks is a very flexible tool it can be hard to fully grasp what it can do (and few people want or need to). However, I have used Swaks in interesting ways, and others have done the same and shared them with me. I hate for these interesting ideas go to waste, so I am including them in this file. =head1 Simple address verification @@ -39,7 +39,7 @@ This allows you to quickly ask a mail server whether a recipient is valid (depen =back -This is a valid email (the mail server returned 250 in response to my RCPT request). This is fairly verbose though. Note the "0" in my command prompt is the exit status of the last command ($?). Since swaks tries to be good about setting a useful return value, I could make vrfy totally silent: +This is a valid email (the mail server returned 250 in response to my RCPT request). This is fairly verbose though. Note the "0" in my command prompt is the exit status of the last command ($?). Since Swaks tries to be good about setting a useful return value, I could make vrfy totally silent: =over 4 @@ -67,9 +67,9 @@ This works well if you use $? in your command prompt, but if not it is less usef =head1 Stress-testing an SMTP server -I've received a couple of different requests and a couple of different patches over the years to add functionality to have swaks fork and send a bunch of emails to a single address. I may still add this functionality some day, but I still feel pretty strongly that ultimately swaks core is a single transaction. Forking multiple swaks seems just outside the scope of the tool. +I've received a couple of different requests and a couple of different patches over the years to add functionality to have Swaks fork and send a bunch of emails to a single address. I may still add this functionality some day, but I still feel pretty strongly that ultimately Swaks core is a single transaction. Forking multiple Swaks seems just outside the scope of the tool. -However, this does seem to be something people want to do. To that end, here is a quick-and-dirty shell script that is (almost) a drop in replacement for swaks: +However, this does seem to be something people want to do. To that end, here is a quick-and-dirty shell script that is (almost) a drop in replacement for Swaks: =over 4 diff --git a/RELEASE/doc/recipes.txt b/RELEASE/doc/recipes.txt index 19fa2472..86f4c00f 100644 --- a/RELEASE/doc/recipes.txt +++ b/RELEASE/doc/recipes.txt @@ -1,6 +1,6 @@ -swaks - Recipes for use - Because swaks is a very flexible tool it can be hard to fully grasp what - it can do (and few people want or need to). However, I have used swaks +Swaks - Recipes for use + Because Swaks is a very flexible tool it can be hard to fully grasp what + it can do (and few people want or need to). However, I have used Swaks in interesting ways, and others have done the same and shared them with me. I hate for these interesting ideas go to waste, so I am including them in this file. @@ -35,7 +35,7 @@ Simple address verification This is a valid email (the mail server returned 250 in response to my RCPT request). This is fairly verbose though. Note the "0" in my command - prompt is the exit status of the last command ($?). Since swaks tries to + prompt is the exit status of the last command ($?). Since Swaks tries to be good about setting a useful return value, I could make vrfy totally silent: @@ -63,15 +63,15 @@ Simple address verification Stress-testing an SMTP server I've received a couple of different requests and a couple of different - patches over the years to add functionality to have swaks fork and send + patches over the years to add functionality to have Swaks fork and send a bunch of emails to a single address. I may still add this functionality some day, but I still feel pretty strongly that ultimately - swaks core is a single transaction. Forking multiple swaks seems just + Swaks core is a single transaction. Forking multiple Swaks seems just outside the scope of the tool. However, this does seem to be something people want to do. To that end, here is a quick-and-dirty shell script that is (almost) a drop in - replacement for swaks: + replacement for Swaks: #! diff --git a/RELEASE/doc/ref.pod b/RELEASE/doc/ref.pod index 1e0bbc93..76f6090b 100644 --- a/RELEASE/doc/ref.pod +++ b/RELEASE/doc/ref.pod @@ -113,21 +113,23 @@ To prevent potential confusion in this document a flag to Swaks is always referr Options can be given to Swaks in three ways. They can be specified in a configuration file, in environment variables, and on the command line. Depending on the specific option and whether an argument is given to it, Swaks may prompt the user for the argument. -When Swaks evaluates its options, it first looks for a configuration file (either in a default location or specified with --config). Then it evaluates any options in environment variables. Finally, it evaluates command line options. At each round of processing, any options set earlier will be overridden. Additionally, any option can be prefixed with "no-" to cause Swaks to forget that the variable had previously been set. This capability is necessary because many options treat defined-but-no-argument differently than not-defined. +When Swaks evaluates its options, it first looks for a configuration file (either in a default location or specified with --config). Then it evaluates any options in environment variables. Finally, it evaluates command line options. At each round of processing, any options set earlier will be overridden. Additionally, any option can be prefixed with "no-" to cause Swaks to forget that the variable had previously been set (either in an earlier round, or earlier in the same round). This capability is necessary because many options treat defined-but-no-argument differently than not-defined. + +As a general rule, if the same option is given multiple time, the last time it is given is the one that will be used. This applies to both intra-method (if "--from user1@example.com --from user2@example.com" is given, user2@example.com will be used) and inter-method (if "from user1@example.com" is given in a config file and "--from user2@example.com" is given on the command line, user2@example.com will be used) The exact mechanism and format for using each of the types is listed below. =over 4 -=item CONFIGURATION FILE +=item CONFIGURATION FILES A configuration file can be used to set commonly-used or abnormally verbose options. By default, Swaks looks in order for $SWAKS_HOME/.swaksrc, $HOME/.swaksrc, and $LOGDIR/.swaksrc. If one of those is found to exist (and --config has not been used) that file is used as the configuration file. -Additionally, a configuration file in a non-default location can be specified using --config. If this is set and not given an argument Swaks will not use any configuration file, including any default file. If --config points to a readable file, it is used as the configuration file, overriding any default that may exist. If it points to a non-readable file and error will be shown and Swaks will exit. +Additionally, a configuration file in a non-default location can be specified using --config. If this is set and not given an argument Swaks will not use any configuration file, including any default file. If --config points to a readable file, it is used as the configuration file, overriding any default that may exist. If it points to a non-readable file an error will be shown and Swaks will exit. A set of "portable" defaults can also be created by adding options to the end of the Swaks program file. As distributed, the last line of Swaks should be "__END__". Any lines added after __END__ will be treated as the contents of a configuration file. This allows a set of user preferences to be automatically copied from server to server in a single file. -If present and configuration files have not been explicitly turned off, the __END__ config is always read. Only one other configuration file will ever be used per single invocation of Swaks, even if multiple configuration files are specified. Specifying the --config option with no argument turns off the processing of both the __END__ config and any actual config files. +If configuration files have not been explicitly turned off, the __END__ config is always read. Only one other configuration file will ever be used per single invocation of Swaks, even if multiple configuration files are specified. If the __END__ config and another config are to be read, the __END__ config will be processed first. Specifying the --config option with no argument turns off the processing of both the __END__ config and any actual config files. In a configuration file lines beginning with a hash (#) are ignored. All other lines are assumed to be an option to Swaks, with the leading dash or dashes optional. Everything after a option line's first space is assumed to be the option's argument and is not shell processed. Therefore, quoting is usually unneeded and will be included literally in the argument. Here is an example of the contents of a configuration file: @@ -139,6 +141,16 @@ In a configuration file lines beginning with a hash (#) are ignored. All other # entire argument. h-From: "Fred Example" +Options specific to configuration file: + +=over 4 + +=item --config [/path/to/config] + +This option provides a path to a specific configuration file to be used. If specified with no argument, no automatically-found configuration file (via $HOME, etc, or __END__) will be processed. If the argument is a valid file, that file will be used as the configuration file (after __END__ config). If option is not a valid, readable file, Swaks will error and exit. This option can be specified multiple times, but only the first time it is specified (in environment variable and the command line search order) will be used. + +=back + =item ENVIRONMENT VARIABLES Options can be supplied via environment variables. The variables are in the form $SWAKS_OPT_name, where name is the name of the option that would be specified on the command line. Because dashes aren't allowed in environment variable names in most UNIX-ish shells, no leading dashes should be used and any dashes inside the option's name should be replaced with underscores. The following would create the same options shown in the configuration file example: @@ -148,6 +160,8 @@ Options can be supplied via environment variables. The variables are in the for Setting a variable to an empty value is the same as specifying it on the command line with no argument. For instance, setting SWAKS_OPT_server="" would cause Swaks to prompt the use for the server to which to connect at each invocation. +Because there is no inherent order in options provided by setting environment variables, the options are sorted before being processed. This is not a great solution, but it at least defines the behavior, which would be otherwise undefined. As an example, if both SWAKS_OPT_from and SWAKS_OPT_f were set, the value from SWAKS_OPT_from would be used, because it sorts after SWAKS_OPT_f. Also as a result of not having an inherent order in environment processing, unsetting options with the "no-" prefix is unreliable. It works if the option being turned off sorts before "no-", but fails if it sorts after. Because "no-" is primarily meant to operate between config types (for instance, unsetting from the command line an option that was set in a config file), this is not likely to be a problem. + In addition to setting the equivalent of command line options, SWAKS_HOME can be set to a directory containing the default .swaksrc to be used. =item COMMAND LINE OPTIONS @@ -182,7 +196,7 @@ Explicitly tell Swaks to use network sockets and specify the hostname or IP addr =item -p, --port [port] -Specify which TCP port on the target is to be used, or prompt if no argument is listed. The argument can be a service name (as retrieved by getservbyname(3)) or a port number. The default port is determined by the --protocol option. See --protocol for more details. +Specify which TCP port on the target is to be used, or prompt if no argument is listed. The argument can be a service name (as retrieved by getservbyname(3)) or a port number. The default port is smtp/25 unless influenced by the --protocol or --tls-on-connect options. =item -li, --local-interface [IP or hostname[:port]] @@ -244,11 +258,11 @@ There is no default value for this option. If no recipients are provided via an =item -f, --from [email-address] -Use argument as envelope-sender for email, or prompt user if no argument specified. The string EE can be supplied to mean the null sender. If user does not specify a sender address a default value is used. The domain-part of the default sender is a best guess at the fully-qualified domain name of the local host. The method of determining the local-part varies. On Windows, Win32::LoginName() is used. On UNIX-ish platforms, the $LOGNAME environment variable is used if it is set. Otherwise getpwuid(3) is used. See also --force-getpwuid. +Use argument as envelope-sender for email, or prompt user if no argument specified. The string EE can be supplied to mean the null sender. If user does not specify a sender address a default value is used. The domain-part of the default sender is a best guess at the fully-qualified domain name of the local host. The method of determining the local-part varies. On Windows, Win32::LoginName() is used. On UNIX-ish platforms, the $LOGNAME environment variable is used if it is set. Otherwise getpwuid(3) is used. See also --force-getpwuid. If Swaks cannot determine a local hostname and the sender address is needed for the transaction, Swaks will error and exit. In this case, a valid string must be provided via this option. =item --ehlo, --lhlo, -h, --helo [helo-string] -String to use as argument to HELO/EHLO/LHLO command, or prompt use if no argument is specified. If this option is not used a best guess at the fully-qualified domain name of the local host is used. If the Sys::Hostname module, which is part of the base distribution, is not available the user will be prompted for a HELO value. Note that Sys::Hostname has been observed to not be able to find the local hostname in certain circumstances. This has the same effect as if Sys::Hostname were unavailable. +String to use as argument to HELO/EHLO/LHLO command, or prompt user if no argument is specified. If this option is not used a best guess at the fully-qualified domain name of the local host is used. If Swaks cannot determine a local hostname and the helo string is needed for the transaction, Swaks will error and exit. In this case, a valid string must be provided via this option. =item -q, --quit, --quit-after [stop-point] @@ -266,9 +280,9 @@ In a STARTTLS (but not tls-on-connect) session, terminate the transaction after =item XCLIENT -Quit after XCLIENT is sent +Quit after XCLIENT is sent. -=item TLS +=item STARTTLS, TLS Quit the transaction immediately following TLS negotiation. Note that this happens in different places depending on whether STARTTLS or tls-on-connect are used. This always quits after the point where TLS would have been negotiated, regardless of whether it was attempted. @@ -292,11 +306,23 @@ Quit after RCPT TO: is sent. =item --da, --drop-after [stop-point] -The option is similar to --quit-after, but instead of trying to cleanly shut down the session it simply terminates the session. This option accepts the same stop-points as --quit-after. +The option is similar to --quit-after, but instead of trying to cleanly shut down the session it simply terminates the session. This option accepts the same stop-points as --quit-after and additionally accepts DATA and DOT, detailed below. + +=over 4 + +=item DATA + +Quit after DATA is sent. + +=item DOT + +Quit after the final '.' of the message is sent. + +=back =item --das, --drop-after-send [stop-point] -This option is similar to --drop-after, but instead of dropping the connection after reading a response to the stop-point, it drops the connection immediately after sending stop-point. +This option is similar to --drop-after, but instead of dropping the connection after reading a response to the stop-point, it drops the connection immediately after sending stop-point. It accepts the same stop-points as --drop-after. =item --timeout [time] @@ -582,7 +608,7 @@ This is the "free form" XCLIENT option. Whatever value is provided for XCLIENT_ The primary advantage to this over the more specific options above is that there is no XCLIENT syntax validation here. This allows you to send invalid XCLIENT to the target server for testing. Additionally, at least one MTA (Message Systems' Momentum, formerly ecelerity) implements XCLIENT without advertising supported attributes. The --xclient option allows you to skip the "supported attributes" check when communicating with this type of MTA (though see also --xclient-no-verify). -The --xclient option can be mixed freely with the --xclient-* options above. If "--xclient-addr 192.168.0.1 --xclient 'FOO=bar NAME=wind'" is given to Swaks, "XCLIENT ADDR=192.168.0.1 FOO=bar NAME=wind" will be sent to the target server. +The --xclient option can be mixed freely with the --xclient-* options above. The argument to --xclient will be sent in its own command group. For instance, if "--xclient-addr 192.168.0.1 --xclient-port 26 --xclient 'FOO=bar NAME=wind'" is given to Swaks, "XCLIENT ADDR=192.168.0.1 PORT=26" and "XCLIENT FOO=bar NAME=wind" will both be sent to the target server. =item --xclient-no-verify @@ -654,15 +680,23 @@ Specify the destination port of the proxied connection. =head1 DATA OPTIONS -These options pertain to the contents for the DATA portion of the SMTP transaction. +These options pertain to the contents for the DATA portion of the SMTP transaction. By default a very simple message is sent. If the --attach or --attach-body options are used, Swaks attempts to upgrade to a MIME message. =over 4 =item -d, --data [data-portion] -Use argument as the entire contents of DATA, or prompt user if no argument specified. If the argument '-' is provided the data will be read from STDIN. If any other argument is provided and it represents the name of an open-able file, the contents of the file will be used. Any other argument will be itself for the DATA contents. +Use argument as the entire contents of DATA. + +If no argument is provided, user will be prompted to supply value. -The value can be on one single line, with \n (ASCII 0x5c, 0x6e) representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. The default value for this option is "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n". +If the argument '-' is provided the data will be read from STDIN with no prompt (same as -g). + +If the argument does not contain any literal (0x0a) or representative (0x5c, 0x6e or %NEWLINE%) newline characters, it will be treated as a filename. If the file is open-able, the contents of the file will be used as the data portion. If the file cannot be opened, Swaks will error and exit. + +Any other argument will be used as the DATA contents. + +The value can be on one single line, with \n (ASCII 0x5c, 0x6e) representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. The default value for this option is "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n". Very basic token parsing is performed on the DATA portion. The following table shows the recognized tokens and their replacement values: @@ -696,6 +730,10 @@ Replaced with the contents of the --add-header option. If --add-header is not s Replaced with the value specified by the --body option. See --body for default. +=item %NEWLINE% + +Replaced with carriage return, newline (0x0d, 0x0a). This is identical to using '\n' (0x5c, 0x6e), but doesn't have the escaping concerns that the backslash can cause on the newline. + =back =item -dab, --dump-as-body [section[,section]] @@ -710,19 +748,23 @@ Cause --dump-as-body to include plaintext passwords. This option is not recomme Specify the body of the email. The default is "This is a test mailing". If no argument to --body is given, prompt to supply one interactively. If '-' is supplied, the body will be read from standard input. If any other text is provided and the text represents an open-able file, the content of that file is used as the body. If it does not represent an open-able file, the text itself is used as the body. -If the message is forced to MIME format (see --attach) the argument to this option will be included unencoded as the first MIME part. Its content-type will always be text/plain. +If the message is forced to MIME format (see --attach) "--body 'body text'" is the same as "--attach-type text/plain --attach-body 'body text'". See --attach-body for details on creating a multipart/alternative body. =item --attach [attachment-specification] 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 respect to STDIN, file contents, etc. --attach can be supplied multiple times to create multiple attachments. By default, each attachment is attached as an application/octet-stream file. See --attach-type for changing this behavior. -If a filename is specified, the MIME encoding will include that file name. See --attach-name for more detail on file naming. +If the contents of the attachment are provided via a file name, the MIME encoding will include that file name. See --attach-name for more detail on file naming. 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-body [body-specification] + +This is a variation on --attach that is specifically for the body part of the email. It behaves identically to --attach in that it takes the same arguments and forces the creation of a MIME message. However, it is different in that the argument will always be the first MIME part in the message, no matter where in option processing order it is encountered. Additionally, --attach-body options stack to allow creation of multipart/alternative bodies. For example, '--attach-type text/plain --attach "plain text body" --attach-type text/html --attach "html body"' would create a multipart/alternative message body. + =item --attach-type [mime-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. +By default, content that gets MIME attached to a message with the --attach option is encoded as application/octet-stream (except for the body, which is text/plain by default). --attach-type changes the mime type for every --attach option which follows it. It can be specified multiple times. The current MIME type gets reset to application/octet-stream between processing body parts and other parts. =item --attach-name [name] @@ -730,7 +772,7 @@ This option sets the filename that will be included in the MIME part created for =item -ah, --add-header [header] -This option allows headers to be added to the DATA. If %H is present in the DATA it is replaced with the argument to this option. If %H is not present, the argument is inserted between the first two consecutive newlines in the DATA (that is, it is inserted at the end of the existing headers). +This option allows headers to be added to the DATA. If %NEW_HEADERS% is present in the DATA it is replaced with the argument to this option. If %NEW_HEADERS% is not present, the argument is inserted between the first two consecutive newlines in the DATA (that is, it is inserted at the end of the existing headers). The option can either be specified multiple times or a single time with multiple headers separated 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. @@ -886,11 +928,11 @@ This option causes Swaks to print the results of option processing, immediately =item --help -Display this help information. +Display this help information and exit. =item --version -Display version information. +Display version information and exit. =back @@ -912,6 +954,24 @@ This program was almost exclusively developed against Exim mail servers. It has =back +=head1 ENVIRONMENT VARIABLES + +=over 4 + +=item LOGNAME + +If Swaks must create a sender address, $LOGNAME is used as the message local-part if it is set, and unless --force-getpwuid is used. + +=item SWAKS_HOME + +Used when searching for a .swaksrc configuration file. See OPTION PROCESSING -> CONFIGURATION FILES above. + +=item SWAKS_OPT_* + +Environment variable prefix used to specify Swaks options from environment variables. See OPTION PROCESSING -> ENVIRONMENT VARIABLES above. + +=back + =head1 EXIT CODES =over 4 diff --git a/RELEASE/doc/ref.txt b/RELEASE/doc/ref.txt index af3a65bf..a2b36bb8 100644 --- a/RELEASE/doc/ref.txt +++ b/RELEASE/doc/ref.txt @@ -120,13 +120,22 @@ OPTION PROCESSING command line options. At each round of processing, any options set earlier will be overridden. Additionally, any option can be prefixed with "no-" to cause Swaks to forget that the variable had previously - been set. This capability is necessary because many options treat + been set (either in an earlier round, or earlier in the same round). + This capability is necessary because many options treat defined-but-no-argument differently than not-defined. + As a general rule, if the same option is given multiple time, the last + time it is given is the one that will be used. This applies to both + intra-method (if "--from user1@example.com --from user2@example.com" is + given, user2@example.com will be used) and inter-method (if "from + user1@example.com" is given in a config file and "--from + user2@example.com" is given on the command line, user2@example.com will + be used) + The exact mechanism and format for using each of the types is listed below. - CONFIGURATION FILE + CONFIGURATION FILES A configuration file can be used to set commonly-used or abnormally verbose options. By default, Swaks looks in order for $SWAKS_HOME/.swaksrc, $HOME/.swaksrc, and $LOGDIR/.swaksrc. If one @@ -138,7 +147,7 @@ OPTION PROCESSING Swaks will not use any configuration file, including any default file. If --config points to a readable file, it is used as the configuration file, overriding any default that may exist. If it - points to a non-readable file and error will be shown and Swaks will + points to a non-readable file an error will be shown and Swaks will exit. A set of "portable" defaults can also be created by adding options @@ -148,12 +157,13 @@ OPTION PROCESSING of user preferences to be automatically copied from server to server in a single file. - If present and configuration files have not been explicitly turned - off, the __END__ config is always read. Only one other configuration - file will ever be used per single invocation of Swaks, even if - multiple configuration files are specified. Specifying the --config - option with no argument turns off the processing of both the __END__ - config and any actual config files. + If configuration files have not been explicitly turned off, the + __END__ config is always read. Only one other configuration file + will ever be used per single invocation of Swaks, even if multiple + configuration files are specified. If the __END__ config and another + config are to be read, the __END__ config will be processed first. + Specifying the --config option with no argument turns off the + processing of both the __END__ config and any actual config files. In a configuration file lines beginning with a hash (#) are ignored. All other lines are assumed to be an option to Swaks, with the @@ -170,6 +180,19 @@ OPTION PROCESSING # entire argument. h-From: "Fred Example" + Options specific to configuration file: + + --config [/path/to/config] + This option provides a path to a specific configuration file to + be used. If specified with no argument, no automatically-found + configuration file (via $HOME, etc, or __END__) will be + processed. If the argument is a valid file, that file will be + used as the configuration file (after __END__ config). If option + is not a valid, readable file, Swaks will error and exit. This + option can be specified multiple times, but only the first time + it is specified (in environment variable and the command line + search order) will be used. + ENVIRONMENT VARIABLES Options can be supplied via environment variables. The variables are in the form $SWAKS_OPT_name, where name is the name of the option @@ -187,6 +210,20 @@ OPTION PROCESSING SWAKS_OPT_server="" would cause Swaks to prompt the use for the server to which to connect at each invocation. + Because there is no inherent order in options provided by setting + environment variables, the options are sorted before being + processed. This is not a great solution, but it at least defines the + behavior, which would be otherwise undefined. As an example, if both + SWAKS_OPT_from and SWAKS_OPT_f were set, the value from + SWAKS_OPT_from would be used, because it sorts after SWAKS_OPT_f. + Also as a result of not having an inherent order in environment + processing, unsetting options with the "no-" prefix is unreliable. + It works if the option being turned off sorts before "no-", but + fails if it sorts after. Because "no-" is primarily meant to operate + between config types (for instance, unsetting from the command line + an option that was set in a config file), this is not likely to be a + problem. + In addition to setting the equivalent of command line options, SWAKS_HOME can be set to a directory containing the default .swaksrc to be used. @@ -248,8 +285,8 @@ TRANSPORTS Specify which TCP port on the target is to be used, or prompt if no argument is listed. The argument can be a service name (as retrieved by getservbyname(3)) or a port number. The default - port is determined by the --protocol option. See --protocol for - more details. + port is smtp/25 unless influenced by the --protocol or + --tls-on-connect options. -li, --local-interface [IP or hostname[:port]] Use argument as the local interface for the outgoing SMTP @@ -337,17 +374,17 @@ PROTOCOL OPTIONS determining the local-part varies. On Windows, Win32::LoginName() is used. On UNIX-ish platforms, the $LOGNAME environment variable is used if it is set. Otherwise getpwuid(3) is used. See also - --force-getpwuid. + --force-getpwuid. If Swaks cannot determine a local hostname and the + sender address is needed for the transaction, Swaks will error and + exit. In this case, a valid string must be provided via this option. --ehlo, --lhlo, -h, --helo [helo-string] - String to use as argument to HELO/EHLO/LHLO command, or prompt use + String to use as argument to HELO/EHLO/LHLO command, or prompt user if no argument is specified. If this option is not used a best guess - at the fully-qualified domain name of the local host is used. If the - Sys::Hostname module, which is part of the base distribution, is not - available the user will be prompted for a HELO value. Note that - Sys::Hostname has been observed to not be able to find the local - hostname in certain circumstances. This has the same effect as if - Sys::Hostname were unavailable. + at the fully-qualified domain name of the local host is used. If + Swaks cannot determine a local hostname and the helo string is + needed for the transaction, Swaks will error and exit. In this case, + a valid string must be provided via this option. -q, --quit, --quit-after [stop-point] Point at which the transaction should be stopped. When the requested @@ -366,9 +403,10 @@ PROTOCOL OPTIONS transaction, behaves the same as HELO (see below). XCLIENT - Quit after XCLIENT is sent + Quit after XCLIENT is sent. - TLS Quit the transaction immediately following TLS negotiation. Note + STARTTLS, TLS + Quit the transaction immediately following TLS negotiation. Note that this happens in different places depending on whether STARTTLS or tls-on-connect are used. This always quits after the point where TLS would have been negotiated, regardless of @@ -392,12 +430,19 @@ PROTOCOL OPTIONS --da, --drop-after [stop-point] The option is similar to --quit-after, but instead of trying to cleanly shut down the session it simply terminates the session. This - option accepts the same stop-points as --quit-after. + option accepts the same stop-points as --quit-after and additionally + accepts DATA and DOT, detailed below. + + DATA + Quit after DATA is sent. + + DOT Quit after the final '.' of the message is sent. --das, --drop-after-send [stop-point] This option is similar to --drop-after, but instead of dropping the connection after reading a response to the stop-point, it drops the - connection immediately after sending stop-point. + connection immediately after sending stop-point. It accepts the same + stop-points as --drop-after. --timeout [time] Use argument as the SMTP transaction timeout, or prompt user if no @@ -821,9 +866,11 @@ XCLIENT OPTIONS see also --xclient-no-verify). The --xclient option can be mixed freely with the --xclient-* - options above. If "--xclient-addr 192.168.0.1 --xclient 'FOO=bar - NAME=wind'" is given to Swaks, "XCLIENT ADDR=192.168.0.1 FOO=bar - NAME=wind" will be sent to the target server. + options above. The argument to --xclient will be sent in its own + command group. For instance, if "--xclient-addr 192.168.0.1 + --xclient-port 26 --xclient 'FOO=bar NAME=wind'" is given to Swaks, + "XCLIENT ADDR=192.168.0.1 PORT=26" and "XCLIENT FOO=bar NAME=wind" + will both be sent to the target server. --xclient-no-verify Do not enforce the requirement that an XCLIENT attribute must be @@ -921,21 +968,32 @@ PROXY OPTIONS DATA OPTIONS These options pertain to the contents for the DATA portion of the SMTP - transaction. + transaction. By default a very simple message is sent. If the --attach + or --attach-body options are used, Swaks attempts to upgrade to a MIME + message. -d, --data [data-portion] - Use argument as the entire contents of DATA, or prompt user if no - argument specified. If the argument '-' is provided the data will be - read from STDIN. If any other argument is provided and it represents - the name of an open-able file, the contents of the file will be - used. Any other argument will be itself for the DATA contents. + Use argument as the entire contents of DATA. + + If no argument is provided, user will be prompted to supply value. + + If the argument '-' is provided the data will be read from STDIN + with no prompt (same as -g). + + If the argument does not contain any literal (0x0a) or + representative (0x5c, 0x6e or %NEWLINE%) newline characters, it will + be treated as a filename. If the file is open-able, the contents of + the file will be used as the data portion. If the file cannot be + opened, Swaks will error and exit. + + Any other argument will be used as the DATA contents. The value can be on one single line, with \n (ASCII 0x5c, 0x6e) representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. The default value for this option is "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\nMessage-Id: - <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION + <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n". Very basic token parsing is performed on the DATA portion. The @@ -971,6 +1029,11 @@ DATA OPTIONS Replaced with the value specified by the --body option. See --body for default. + %NEWLINE% + Replaced with carriage return, newline (0x0d, 0x0a). This is + identical to using '\n' (0x5c, 0x6e), but doesn't have the + escaping concerns that the backslash can cause on the newline. + -dab, --dump-as-body [section[,section]] If --dump-as-body is used and no other option is used to change the default body of the message, the body is replaced with output @@ -995,9 +1058,10 @@ DATA OPTIONS the body. If it does not represent an open-able file, the text itself is used as the body. - If the message is forced to MIME format (see --attach) the argument - to this option will be included unencoded as the first MIME part. - Its content-type will always be text/plain. + If the message is forced to MIME format (see --attach) "--body 'body + text'" is the same as "--attach-type text/plain --attach-body 'body + text'". See --attach-body for details on creating a + multipart/alternative body. --attach [attachment-specification] When one or more --attach option is supplied, the message is changed @@ -1008,8 +1072,9 @@ DATA OPTIONS application/octet-stream file. See --attach-type for changing this behavior. - If a filename is specified, the MIME encoding will include that file - name. See --attach-name for more detail on file naming. + If the contents of the attachment are provided via a file name, the + MIME encoding will include that file name. See --attach-name for + more detail on file naming. It is legal for '-' (STDIN) to be specified as an argument multiple times (once for --body and multiple times for --attach). In this @@ -1017,11 +1082,26 @@ DATA OPTIONS This is useful for attaching the same content with multiple MIME types. + --attach-body [body-specification] + This is a variation on --attach that is specifically for the body + part of the email. It behaves identically to --attach in that it + takes the same arguments and forces the creation of a MIME message. + However, it is different in that the argument will always be the + first MIME part in the message, no matter where in option processing + order it is encountered. Additionally, --attach-body options stack + to allow creation of multipart/alternative bodies. For example, + '--attach-type text/plain --attach "plain text body" --attach-type + text/html --attach "html body"' would create a multipart/alternative + message body. + --attach-type [mime-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. + --attach option is encoded as application/octet-stream (except for + the body, which is text/plain by default). --attach-type changes the + mime type for every --attach option which follows it. It can be + specified multiple times. The current MIME type gets reset to + application/octet-stream between processing body parts and other + parts. --attach-name [name] This option sets the filename that will be included in the MIME part @@ -1031,11 +1111,11 @@ DATA OPTIONS name. -ah, --add-header [header] - This option allows headers to be added to the DATA. If %H is present - in the DATA it is replaced with the argument to this option. If %H - is not present, the argument is inserted between the first two - consecutive newlines in the DATA (that is, it is inserted at the end - of the existing headers). + This option allows headers to be added to the DATA. If %NEW_HEADERS% + is present in the DATA it is replaced with the argument to this + option. If %NEW_HEADERS% is not present, the argument is inserted + between the first two consecutive newlines in the DATA (that is, it + is inserted at the end of the existing headers). The option can either be specified multiple times or a single time with multiple headers separated by a literal '\n' string. So, @@ -1231,10 +1311,10 @@ OUTPUT OPTIONS provided, all sections are displayed --help - Display this help information. + Display this help information and exit. --version - Display version information. + Display version information and exit. PORTABILITY OPERATING SYSTEMS @@ -1262,6 +1342,21 @@ PORTABILITY with any fairly modern mail server. If a problem is found, please alert the author at the address below. +ENVIRONMENT VARIABLES + LOGNAME + If Swaks must create a sender address, $LOGNAME is used as the + message local-part if it is set, and unless --force-getpwuid is + used. + + SWAKS_HOME + Used when searching for a .swaksrc configuration file. See OPTION + PROCESSING -> CONFIGURATION FILES above. + + SWAKS_OPT_* + Environment variable prefix used to specify Swaks options from + environment variables. See OPTION PROCESSING -> ENVIRONMENT + VARIABLES above. + EXIT CODES 0 no errors occurred diff --git a/RELEASE/swaks b/RELEASE/swaks index 8a83e7cc..cd9c1657 100755 --- a/RELEASE/swaks +++ b/RELEASE/swaks @@ -13,10 +13,10 @@ use strict; $| = 1; my($p_name) = $0 =~ m|/?([^/]+)$|; -my $p_version = build_version("20181104.0", '$Id$'); +my $p_version = build_version("20190914.0", '$Id$'); my $p_usage = "Usage: $p_name [--help|--version] (see --help for details)"; my $p_cp = <<'EOM'; - Copyright (c) 2003-2008,2010-2018 John Jetmore + 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 @@ -33,12 +33,12 @@ my $p_cp = <<'EOM'; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. EOM -# before we do anything else, check for --help -ext_usage(); - # Get all input provided to our program, via file, env, command line, etc my %O = %{ load_args() }; +# before we do anything else, check for --help and --version +ext_usage(); + # Get our functional dependencies and then print and exit early if requested load_dependencies(); if ($O{get_support}) { @@ -307,10 +307,14 @@ sub sendmail { do_smtp_quit(1, 0) if ($G::quit_after eq 'rcpt'); # 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'); # 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 QUIT do_smtp_quit(0) || do_smtp_quit(1, 27); @@ -1233,9 +1237,11 @@ sub transact { 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)) { @@ -1269,16 +1275,14 @@ sub transact { alarm(0); }; - 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(); - } + 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; @@ -1610,310 +1614,313 @@ sub load_dependencies { sub get_option_struct { @G::raw_option_data = ( # location of config file - { opts => ['config'], suffix => ':s', - okey => 'config_file', type => 'scalar', }, + { opts => ['config'], suffix => ':s', + okey => 'config_file', type => 'scalar', }, # envelope-(f)rom address - { opts => ['f', 'from'], suffix => ':s', - okey => 'mail_from', type => 'scalar', }, + { opts => ['f', 'from'], suffix => ':s', + okey => 'mail_from', type => 'scalar', }, # envelope-(t)o address - { opts => ['t', 'to'], suffix => ':s', - okey => 'mail_to', type => 'scalar', }, + { opts => ['t', 'to'], suffix => ':s', + okey => 'mail_to', type => 'scalar', }, # (h)elo string - { opts => ['h', 'helo', 'ehlo', 'lhlo'], suffix => ':s', - okey => 'mail_helo', type => 'scalar', }, + { opts => ['h', 'helo', 'ehlo', 'lhlo'], suffix => ':s', + okey => 'mail_helo', type => 'scalar', }, # (s)erver to use - { opts => ['s', 'server'], suffix => ':s', - okey => 'mail_server', type => 'scalar', }, + { opts => ['s', 'server'], suffix => ':s', + okey => 'mail_server', type => 'scalar', }, # force ipv4 only - { opts => ['4'], suffix => '', - okey => 'force_ipv4', type => 'scalar', }, + { opts => ['4'], suffix => '', + okey => 'force_ipv4', type => 'scalar', }, # force ipv6 only - { opts => ['6'], suffix => '', - okey => 'force_ipv6', type => 'scalar', }, + { opts => ['6'], suffix => '', + okey => 'force_ipv6', type => 'scalar', }, # copy MX/routing from another domain - { opts => ['copy-routing'], suffix => ':s', - okey => 'copy_routing', type => 'scalar', }, + { opts => ['copy-routing'], suffix => ':s', + okey => 'copy_routing', type => 'scalar', }, # (p)ort to use - { opts => ['p', 'port'], suffix => ':s', - okey => 'mail_port', type => 'scalar', }, + { opts => ['p', 'port'], suffix => ':s', + okey => 'mail_port', type => 'scalar', }, # protocol to use (smtp, esmtp, lmtp) - { opts => ['protocol'], suffix => ':s', - okey => 'mail_protocol', type => 'scalar', }, + { opts => ['protocol'], suffix => '=s', + okey => 'mail_protocol', type => 'scalar', }, # (d)ata portion ('\n' for newlines) - { opts => ['d', 'data'], suffix => ':s', - okey => 'mail_data', type => 'scalar', }, + { opts => ['d', 'data'], suffix => ':s', + okey => 'mail_data', type => 'scalar', }, # use the --dump text as default body - { opts => ['dab', 'dump-as-body'], suffix => ':s', - okey => 'dump_as_body', type => 'scalar', }, + { opts => ['dab', 'dump-as-body'], suffix => ':s', + okey => 'dump_as_body', type => 'scalar', }, # implies --dump-as-body; forces raw passwords to be used - { opts => ['dabsp', 'dump-as-body-shows-password'], suffix => '', - okey => 'dab_sp', type => 'scalar', }, + { opts => ['dabsp', 'dump-as-body-shows-password'], suffix => '', + okey => 'dab_sp', type => 'scalar', }, # timeout for each trans (def 30s) - { opts => ['timeout'], suffix => ':s', - okey => 'timeout', type => 'scalar', }, + { opts => ['timeout'], suffix => ':s', + okey => 'timeout', type => 'scalar', }, # (g)et data on stdin - { opts => ['g'], suffix => '', - okey => 'data_on_stdin', type => 'scalar', }, + { opts => ['g'], suffix => '', + okey => 'data_on_stdin', type => 'scalar', }, # (q)uit after - { opts => ['q', 'quit', 'quit-after'], suffix => '=s', - okey => 'quit_after', type => 'scalar', }, + { opts => ['q', 'quit', 'quit-after'], suffix => '=s', + okey => 'quit_after', type => 'scalar', }, # drop after (don't quit, just drop) - { opts => ['da', 'drop', 'drop-after'], suffix => '=s', - okey => 'drop_after', type => 'scalar', }, + { opts => ['da', 'drop', 'drop-after'], suffix => '=s', + okey => 'drop_after', type => 'scalar', }, # drop after send (between send and read) - { opts => ['das', 'drop-after-send'], suffix => '=s', - okey => 'drop_after_send', type => 'scalar', }, + { opts => ['das', 'drop-after-send'], suffix => '=s', + okey => 'drop_after_send', type => 'scalar', }, # do (n)ot print data portion - { opts => ['n', 'suppress-data'], suffix => '', - okey => 'suppress_data', type => 'scalar', }, + { opts => ['n', 'suppress-data'], suffix => '', + okey => 'suppress_data', type => 'scalar', }, # force auth, exit if not supported - { opts => ['a', 'auth'], suffix => ':s', - okey => 'auth', type => 'scalar', }, + { opts => ['a', 'auth'], suffix => ':s', + okey => 'auth', type => 'scalar', }, # user for auth - { opts => ['au', 'auth-user'], suffix => ':s', - okey => 'auth_user', type => 'scalar', }, + { opts => ['au', 'auth-user'], suffix => ':s', + okey => 'auth_user', type => 'scalar', }, # pass for auth - { opts => ['ap', 'auth-password'], suffix => ':s', - okey => 'auth_pass', type => 'scalar', }, + { opts => ['ap', 'auth-password'], suffix => ':s', + okey => 'auth_pass', type => 'scalar', }, # auth type map - { opts => ['am', 'auth-map'], suffix => '=s', - okey => 'auth_map', type => 'scalar', }, + { opts => ['am', 'auth-map'], suffix => '=s', + okey => 'auth_map', type => 'scalar', }, # extra, authenticator-specific options - { opts => ['ae', 'auth-extra'], suffix => '=s', - okey => 'auth_extra', type => 'scalar', }, + { opts => ['ae', 'auth-extra'], suffix => '=s', + okey => 'auth_extra', type => 'scalar', }, # hide passwords when possible - { opts => ['ahp', 'auth-hide-password'], suffix => ':s', - okey => 'auth_hidepw', type => 'scalar', }, + { opts => ['ahp', 'auth-hide-password'], suffix => ':s', + okey => 'auth_hidepw', type => 'scalar', }, # translate base64 strings - { opts => ['apt', 'auth-plaintext'], suffix => '', - okey => 'auth_showpt', type => 'scalar', }, + { opts => ['apt', 'auth-plaintext'], suffix => '', + okey => 'auth_showpt', type => 'scalar', }, # auth optional (ignore failure) - { opts => ['ao', 'auth-optional'], suffix => ':s', - okey => 'auth_optional', type => 'scalar', }, + { opts => ['ao', 'auth-optional'], suffix => ':s', + okey => 'auth_optional', type => 'scalar', }, # req auth if avail - { opts => ['aos', 'auth-optional-strict'], suffix => ':s', - okey => 'auth_optional_strict', type => 'scalar', }, + { opts => ['aos', 'auth-optional-strict'], suffix => ':s', + okey => 'auth_optional_strict', type => 'scalar', }, # report capabilties - { opts => ['support'], suffix => '', - okey => 'get_support', type => 'scalar', }, + { opts => ['support'], suffix => '', + okey => 'get_support', type => 'scalar', }, # local interface to use - { opts => ['li', 'local-interface'], suffix => ':s', - okey => 'lint', type => 'scalar', }, + { opts => ['li', 'local-interface'], suffix => ':s', + okey => 'lint', type => 'scalar', }, # local port - { opts => ['lp', 'local-port', 'lport'], suffix => ':s', - okey => 'lport', type => 'scalar', }, + { opts => ['lp', 'local-port', 'lport'], suffix => ':s', + okey => 'lport', type => 'scalar', }, # use TLS - { opts => ['tls'], suffix => '', - okey => 'tls', type => 'scalar', }, + { opts => ['tls'], suffix => '', + okey => 'tls', type => 'scalar', }, # use tls if available - { opts => ['tlso', 'tls-optional'], suffix => '', - okey => 'tls_optional', type => 'scalar', }, + { opts => ['tlso', 'tls-optional'], suffix => '', + okey => 'tls_optional', type => 'scalar', }, # req tls if avail - { opts => ['tlsos', 'tls-optional-strict'], suffix => ':s', - okey => 'tls_optional_strict', type => 'scalar', }, + { opts => ['tlsos', 'tls-optional-strict'], suffix => '', + okey => 'tls_optional_strict', type => 'scalar', }, # use tls if available - { opts => ['tlsc', 'tls-on-connect'], suffix => '', - okey => 'tls_on_connect', type => 'scalar', }, + { opts => ['tlsc', 'tls-on-connect'], suffix => '', + okey => 'tls_on_connect', type => 'scalar', }, # local cert to present to server - { opts => ['tls-cert'], suffix => '=s', - okey => 'tls_cert', type => 'scalar', }, + { opts => ['tls-cert'], suffix => '=s', + okey => 'tls_cert', type => 'scalar', }, # local key to present to server - { opts => ['tls-key'], suffix => '=s', - okey => 'tls_key', type => 'scalar', }, + { opts => ['tls-key'], suffix => '=s', + okey => 'tls_key', type => 'scalar', }, # local key to present to server - { opts => ['tlsp', 'tls-protocol'], suffix => '=s', - okey => 'tls_protocol', type => 'scalar', }, + { opts => ['tlsp', 'tls-protocol'], suffix => '=s', + okey => 'tls_protocol', type => 'scalar', }, # local key to present to server - { opts => ['tls-cipher'], suffix => '=s', - okey => 'tls_cipher', type => 'scalar', }, + { opts => ['tls-cipher'], suffix => '=s', + okey => 'tls_cipher', type => 'scalar', }, # save tls peer certificate - { opts => ['tls-get-peer-cert'], suffix => ':s', - okey => 'tls_get_peer_cert', type => 'scalar', }, + { opts => ['tls-get-peer-cert'], suffix => ':s', + okey => 'tls_get_peer_cert', type => 'scalar', }, # require verification of server certificate - { opts => ['tls-verify'], suffix => '', - okey => 'tls_verify', type => 'scalar', }, + { opts => ['tls-verify'], suffix => '', + okey => 'tls_verify', type => 'scalar', }, # local key to present to server - { opts => ['tls-ca-path'], suffix => '=s', - okey => 'tls_ca_path', type => 'scalar', }, + { opts => ['tls-ca-path'], suffix => '=s', + okey => 'tls_ca_path', type => 'scalar', }, # suppress output to varying degrees - { opts => ['S', 'silent'], suffix => ':i', - okey => 'silent', type => 'sub', - literal => 'sub { $r->{silent} = $_[1] || 1; }', }, + { opts => ['S', 'silent'], suffix => ':i', + literal => 'sub { $r->{silent} = $_[1] || 1; }', + okey => 'silent', type => 'sub', }, # Don't strip From_ line from DATA - { opts => ['nsf', 'no-strip-from'], suffix => '', - okey => 'no_strip_from', type => 'scalar', }, - # Use the old, one-character substitution tokens (%H, %D, etc) - { opts => ['use-old-data-tokens'], suffix => '', - okey => 'use_old_data_tokens', type => 'scalar', }, + { opts => ['nsf', 'no-strip-from'], suffix => '', + okey => 'no_strip_from', type => 'scalar', }, # Don't show send/receive hints (legacy) - { opts => ['nth', 'no-hints'], suffix => '', - okey => 'no_hints', type => 'scalar', }, + { opts => ['nth', 'no-hints'], suffix => '', + okey => 'no_hints', type => 'scalar', }, # Don't show transaction hints - { opts => ['nsh', 'no-send-hints'], suffix => '', - okey => 'no_hints_send', type => 'scalar', }, + { opts => ['nsh', 'no-send-hints'], suffix => '', + okey => 'no_hints_send', type => 'scalar', }, # Don't show transaction hints - { opts => ['nrh', 'no-receive-hints'], suffix => '', - okey => 'no_hints_recv', type => 'scalar', }, + { opts => ['nrh', 'no-receive-hints'], suffix => '', + okey => 'no_hints_recv', type => 'scalar', }, # Don't show transaction hints - { opts => ['nih', 'no-info-hints'], suffix => '', - okey => 'no_hints_info', type => 'scalar', }, + { opts => ['nih', 'no-info-hints'], suffix => '', + okey => 'no_hints_info', type => 'scalar', }, # Don't show reception lines - { opts => ['hr', 'hide-receive'], suffix => '', - okey => 'hide_receive', type => 'scalar', }, + { opts => ['hr', 'hide-receive'], suffix => '', + okey => 'hide_receive', type => 'scalar', }, # Don't show sending lines - { opts => ['hs', 'hide-send'], suffix => '', - okey => 'hide_send', type => 'scalar', }, + { opts => ['hs', 'hide-send'], suffix => '', + okey => 'hide_send', type => 'scalar', }, # Don't echo input on potentially sensitive prompts - { opts => ['pp', 'protect-prompt'], suffix => '', - okey => 'protect_prompt', type => 'scalar', }, + { opts => ['pp', 'protect-prompt'], suffix => '', + okey => 'protect_prompt', type => 'scalar', }, # Don't show any swaks-generated, non-error informational lines - { opts => ['hi', 'hide-informational'], suffix => '', - okey => 'hide_informational', type => 'scalar', }, + { opts => ['hi', 'hide-informational'], suffix => '', + okey => 'hide_informational', type => 'scalar', }, # Don't send any output to the terminal - { opts => ['ha', 'hide-all'], suffix => '', - okey => 'hide_all', type => 'scalar', }, + { opts => ['ha', 'hide-all'], suffix => '', + okey => 'hide_all', type => 'scalar', }, # print lapse for send/recv - { opts => ['stl', 'show-time-lapse'], suffix => ':s', - okey => 'show_time_lapse', type => 'scalar', }, + { opts => ['stl', 'show-time-lapse'], suffix => ':s', + okey => 'show_time_lapse', type => 'scalar', }, + # print version and exit + { opts => ['version'], suffix => '', + okey => 'version', type => 'scalar', }, + # print help and exit + { opts => ['help'], suffix => '', + okey => 'help', type => 'scalar', }, # don't touch the data - { opts => ['ndf', 'no-data-fixup'], suffix => '', - okey => 'no_data_fixup', type => 'scalar', }, + { opts => ['ndf', 'no-data-fixup'], suffix => '', + okey => 'no_data_fixup', type => 'scalar', }, # show dumps of the raw read/written text - { opts => ['raw', 'show-raw-text'], suffix => '', - okey => 'show_raw_text', type => 'scalar', }, + { opts => ['raw', 'show-raw-text'], suffix => '', + okey => 'show_raw_text', type => 'scalar', }, # specify file to write to - { opts => ['output-file', 'output'], suffix => '=s', - okey => 'output_file', type => 'scalar', }, + { opts => ['output-file', 'output'], suffix => '=s', + okey => 'output_file', type => 'scalar', }, # specify file to write to - { opts => ['output-file-stdout'], suffix => '=s', - okey => 'output_file_stdout', type => 'scalar', }, + { opts => ['output-file-stdout'], suffix => '=s', + okey => 'output_file_stdout', type => 'scalar', }, # specify file to write to - { opts => ['output-file-stderr'], suffix => '=s', - okey => 'output_file_stderr', type => 'scalar', }, + { opts => ['output-file-stderr'], suffix => '=s', + okey => 'output_file_stderr', type => 'scalar', }, # command to communicate with - { opts => ['pipe'], suffix => ':s', - okey => 'pipe_cmd', type => 'scalar', }, + { opts => ['pipe'], suffix => ':s', + okey => 'pipe_cmd', type => 'scalar', }, # unix domain socket to talk to - { opts => ['socket'], suffix => ':s', - okey => 'socket', type => 'scalar', }, + { opts => ['socket'], suffix => ':s', + okey => 'socket', type => 'scalar', }, # the content of the body of the DATA - { opts => ['body'], suffix => ':s', - okey => 'body_822', type => 'scalar', }, + { opts => ['body'], suffix => ':s', + okey => 'body_822', type => 'scalar', }, # A file to attach - { opts => ['attach-name','attach-type','attach'], suffix => ':s', - okey => 'attach_822', type => 'list', }, + { opts => ['attach-name','attach-type','attach','attach-body'], suffix => ':s', + okey => 'attach_822', type => 'list', }, # replacement for %NEW_HEADERS% DATA token - { opts => ['ah', 'add-header'], suffix => ':s', - okey => 'add_header', type => 'list', }, + { opts => ['ah', 'add-header'], suffix => ':s', + okey => 'add_header', type => 'list', }, # replace header if exist, else add - { opts => ['header'], suffix => ':s', - okey => 'header', type => 'list', }, + { opts => ['header'], suffix => ':s', + okey => 'header', type => 'list', }, # build options and dump - { opts => ['dump'], suffix => ':s', - okey => 'dump_args', type => 'scalar', }, + { opts => ['dump'], suffix => ':s', + okey => 'dump_args', type => 'scalar', }, # build options and dump the generate message body (EML) - { opts => ['dump-mail'], suffix => '', - okey => 'dump_mail', type => 'scalar', }, + { opts => ['dump-mail'], suffix => '', + okey => 'dump_mail', type => 'scalar', }, # attempt PIPELINING - { opts => ['pipeline'], suffix => '', - okey => 'pipeline', type => 'scalar', }, + { opts => ['pipeline'], suffix => '', + okey => 'pipeline', type => 'scalar', }, # attempt PRDR - { opts => ['prdr'], suffix => '', - okey => 'prdr', type => 'scalar', }, + { opts => ['prdr'], suffix => '', + okey => 'prdr', type => 'scalar', }, # use getpwuid building -f - { opts => ['force-getpwuid'], suffix => '', - okey => 'force_getpwuid', type => 'scalar', }, + { opts => ['force-getpwuid'], suffix => '', + 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', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT NAME - { opts => ['xclient-name'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-name'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT ADDR - { opts => ['xclient-addr'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-addr'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT PORT - { opts => ['xclient-port'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-port'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT PROTO - { opts => ['xclient-proto'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-proto'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT DESTADDR - { opts => ['xclient-destaddr'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-destaddr'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT DESTPORT - { opts => ['xclient-destport'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-destport'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT HELO - { opts => ['xclient-helo'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-helo'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT LOGIN - { opts => ['xclient-login'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-login'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT REVERSE_NAME - { opts => ['xclient-reverse-name'], suffix => ':s', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-reverse-name'], suffix => ':s', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # XCLIENT delimiter. Used to indicate that user wants to start a new xclient attr grouping - { opts => ['xclient-delim'], suffix => '', - okey => 'xclient_attrs', type => 'sub', - literal => 'sub { option_callback($r, "xclient_attrs", @_); }', }, + { opts => ['xclient-delim'], suffix => '', + literal => 'sub { option_callback($r, "xclient_attrs", @_); }', + okey => 'xclient_attrs', type => 'sub', }, # if set, XCLIENT will proceed even if XCLIENT not advertised - { opts => ['xclient-optional'], suffix => '', - okey => 'xclient_optional', type => 'scalar', }, + { opts => ['xclient-optional'], suffix => '', + okey => 'xclient_optional', type => 'scalar', }, # proceed if xclient not offered, but fail if offered and not accepted - { opts => ['xclient-optional-strict'], suffix => '', - okey => 'xclient_optional_strict', type => 'scalar', }, + { opts => ['xclient-optional-strict'], suffix => '', + 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 => '', - okey => 'xclient_before_starttls', type => 'scalar', }, + { opts => ['xclient-before-starttls'], suffix => '', + okey => 'xclient_before_starttls', type => 'scalar', }, # Don't require that the --xclient-ATTR attributes be advertised by server - { opts => ['xclient-no-verify'], suffix => '', - okey => 'xclient_no_verify', type => 'scalar', }, + { opts => ['xclient-no-verify'], suffix => '', + 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', }, + # { opts => ['xclient-after'], suffix => ':s', + # okey => 'xclient_after', type => 'scalar', }, # PROXY # argument is the raw PROXY string - { opts => ['proxy'], suffix => ':s', - okey => 'proxy_raw', type => 'scalar', }, + { opts => ['proxy'], suffix => ':s', + okey => 'proxy_raw', type => 'scalar', }, # PROXY version (1 or 2) - { opts => ['proxy-version'], suffix => ':s', - okey => 'proxy_version', type => 'scalar', }, + { opts => ['proxy-version'], suffix => ':s', + okey => 'proxy_version', type => 'scalar', }, # PROXY protocol family (TCP4 or TCP6) - { opts => ['proxy-family'], suffix => ':s', - okey => 'proxy_family', type => 'scalar', }, + { opts => ['proxy-family'], suffix => ':s', + okey => 'proxy_family', type => 'scalar', }, # PROXY protocol command (LOCAL or PROXY) - { opts => ['proxy-command'], suffix => ':s', - okey => 'proxy_command', type => 'scalar', }, + { opts => ['proxy-command'], suffix => ':s', + okey => 'proxy_command', type => 'scalar', }, # PROXY transport protocol - { opts => ['proxy-protocol'], suffix => ':s', - okey => 'proxy_protocol', type => 'scalar', }, + { opts => ['proxy-protocol'], suffix => ':s', + okey => 'proxy_protocol', type => 'scalar', }, # PROXY source address (IPv4 or IPv6) - { opts => ['proxy-source'], suffix => ':s', - okey => 'proxy_source', type => 'scalar', }, + { opts => ['proxy-source'], suffix => ':s', + okey => 'proxy_source', type => 'scalar', }, # PROXY source port - { opts => ['proxy-source-port'], suffix => ':s', - okey => 'proxy_source_port', type => 'scalar', }, + { opts => ['proxy-source-port'], suffix => ':s', + okey => 'proxy_source_port', type => 'scalar', }, # PROXY destination address (IPv4 or IPv6) - { opts => ['proxy-dest'], suffix => ':s', - okey => 'proxy_dest', type => 'scalar', }, + { opts => ['proxy-dest'], suffix => ':s', + okey => 'proxy_dest', type => 'scalar', }, # PROXY destination port - { opts => ['proxy-dest-port'], suffix => ':s', - okey => 'proxy_dest_port', type => 'scalar', }, + { opts => ['proxy-dest-port'], suffix => ':s', + okey => 'proxy_dest_port', type => 'scalar', }, ); return(\@G::raw_option_data); @@ -1942,42 +1949,51 @@ sub load_args { # 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 ($ENV{$v}) { $config_file = "$ENV{$v}/.swaksrc"; last; } + 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" - $config_file = ""; + $skip_config = 1; } else { $config_file = $ENV{SWAKS_OPT_config}; $config_is_default = 0; } } - # lastly go through original command line looking for config file - for (my $i = 0; $i < scalar(@real_ARGV); $i++) { + # lastly go (backwards) through original command line looking for config file, + # choosing the first one found (meaning last one specified) + for (my $i = scalar(@real_ARGV) - 1; $i >= 0; $i--) { if ($real_ARGV[$i] =~ /^-?-config$/) { - if ($real_ARGV[$i+1] =~ /^-/) { - $config_file = ""; + if ($i == scalar(@real_ARGV) - 1 || $real_ARGV[$i+1] =~ /^-/) { + $skip_config = 1; } else { $config_file = $real_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. - if ($config_file) { - foreach my $configf ('&DATA', $config_file) { + # 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) { if (open(C, "<$configf")) { # "#" in col 0 is a comment while (defined(my $m = )) { next if ($m =~ m|^#|); chomp($m); - $m = '-' . $m if ($m !~ /^-/); + $m = '--' . $m if ($m !~ /^-/); push(@ARGV, split(/\s/, $m, 2)); } close(C); @@ -1996,10 +2012,10 @@ sub load_args { # OK, %ARGS contains all the settings from the config file. No do it again # with SWAKS_OPT_* environment variables @ARGV = (); - foreach my $v (keys %ENV) { + foreach my $v (sort keys %ENV) { if ($v =~ m|^SWAKS_OPT_(.*)$|) { my $tv = $1; $tv =~ s|_|-|g; - push(@ARGV, "-$tv", $ENV{$v}); + push(@ARGV, "--$tv", $ENV{$v}); } } fetch_args(\%ARGS, $option_list) if (scalar(@ARGV)); @@ -2021,6 +2037,13 @@ sub fetch_args { # also see if there are any --no- options that need to be processed RUNOPTS: for (my $i = 0; $i < scalar(@ARGV); $i++) { + # before doing any option processing, massage from the optional '--option=arg' format into '--option arg' format. + if ($ARGV[$i] =~ /^(-[^=]+)=(.*)$/) { + $ARGV[$i] = $1; + splice(@ARGV, $i+1, 0, $2); + } + + # if we're referencing an option which was previously marked for removal, remove the removal flag if ($ARGV[$i] =~ /^-?-(.*)$/) { my $opt_name = $1; foreach my $e (@$l) { @@ -2029,9 +2052,12 @@ sub fetch_args { } } } + if ($ARGV[$i] =~ /^-?-h(?:eader)?-([^:]+):?$/) { + # rewrite '--header-Foo bar' into '--header "Foo: bar"' $ARGV[$i] = "--header"; $ARGV[$i+1] = "$1: $ARGV[$i+1]"; } elsif ($ARGV[$i] =~ /^-?-(no-(.*))$/) { + # allow --no-OPTION to remove any previous OPTION settings my $whole_name = $1; my $opt_name = $2; # loop through each of the options and see if it matches foreach my $e (@$l) { @@ -2046,17 +2072,21 @@ sub fetch_args { } ptrans(12, "Unknown option $ARGV[$i], Exiting"); exit(1); - } elsif ($ARGV[$i] =~ /^-?-attach-name$/) { - if ($ARGV[$i+1] =~ /^-/) { + } + elsif ($ARGV[$i] =~ /^-?-(attach.*)$/) { + # all of the --attach* options end up in the same array because the order we receive them in + # is important. But we only get the value, so flag them so we will be able to tell which + # argument belongs with which option later + + my $opt = uc($1); + if ($ARGV[$i] =~ /^-?-attach-name$/ && $ARGV[$i+1] =~ /^-/) { # in this case, the user is trying to tell us not to use # a filename for the next attachment. This means we actually have to # insert an argument, flagged as a filename, with no value - # XXX WCSFIXME - splice(@ARGV, $i+1, 0, "FILENAME:"); - } else { - # We were given a filename - mark it as such so we can figure - # it out later - $ARGV[$i+1] = "FILENAME:" . $ARGV[$i+1]; + splice(@ARGV, $i+1, 0, 'SWAKS_ATTACH-NAME:'); + } + else { + $ARGV[$i+1] = 'SWAKS_' . $opt . ':' . $ARGV[$i+1]; } } } @@ -2086,7 +2116,7 @@ sub fetch_args { exit(1); } - Getopt::Long::Configure("bundling_override"); + Getopt::Long::Configure("no_ignore_case"); GetOptions(%options) || exit(1); # now remove any options that might have been added by a --no option @@ -2094,10 +2124,8 @@ sub fetch_args { # in the same context (config file, command line, env var) if (scalar keys %to_delete) { foreach my $e (@$l) { - foreach my $opt_name (@{$e->{opts}}) { - if ($to_delete{$opt_name}) { - delete($r->{$e->{okey}}); - } + if ($to_delete{$e->{okey}}) { + delete($r->{$e->{okey}}); } } } @@ -2179,7 +2207,7 @@ sub process_args { $G::trans_fh_ef = "STDERR"; if ($o->{output_file} && (!$o->{output_file_stderr} || !$o->{output_file_stdout})) { if (!open(OUTFH, ">>$o->{output_file}")) { - ptrans(12, "Unable to open $o->{output_file} for writing\n"); + ptrans(12, "Unable to open $o->{output_file} for writing"); exit(1); } $G::trans_fh_oh = \*OUTFH; @@ -2189,7 +2217,7 @@ sub process_args { } if ($o->{output_file_stderr}) { if (!open(OUTEFH, ">>$o->{output_file_stderr}")) { - ptrans(12, "Unable to open $o->{output_file_stderr} for writing\n"); + ptrans(12, "Unable to open $o->{output_file_stderr} for writing"); exit(1); } $G::trans_fh_eh = \*OUTEFH; @@ -2197,7 +2225,7 @@ sub process_args { } if ($o->{output_file_stdout}) { if (!open(OUTOFH, ">>$o->{output_file_stdout}")) { - ptrans(12, "Unable to open $o->{output_file_stdout} for writing\n"); + ptrans(12, "Unable to open $o->{output_file_stdout} for writing"); exit(1); } $G::trans_fh_oh = \*OUTOFH; @@ -2288,22 +2316,38 @@ sub process_args { 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 '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 _requires_ a to address - $G::server_only = 1 if (${"G::$opt"} ne 'rcpt'); + # only rcpt, data, and dot _require_ a to address + $G::server_only = 1 if (${"G::$opt"} !~ /^(rcpt|data|dot)$/); + + # 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"} = ''; } } # 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); + if (defined($o->{show_time_lapse})) { + if (length($o->{show_time_lapse}) && $o->{show_time_lapse} !~ /^i/i) { + ptrans(12, "Unknown argument '$o->{show_time_lapse}' to option show-time-lapse, exiting"); + exit(1); + } + if (avail("hires_timing") && $o->{show_time_lapse} !~ /^i/i) { + $G::show_time_lapse = 'hires'; + } + else { + $G::show_time_lapse = 'integer'; + } + } # pipe command, if one is specified $G::link{process} = $o->{pipe_cmd} || interact("Pipe: ", '^.+$') @@ -2324,9 +2368,16 @@ sub process_args { # SMTP mail from $n{from} = $o->{mail_from} || interact("From: ", '^.*$') if (defined($o->{mail_from})); - $n{from} ||= ($hostname || ($G::server_only && $G::quit_after ne 'mail' && $G::drop_after ne 'mail' && $G::drop_after_send ne 'mail') - ? "$user\@$hostname" - : interact("From: ", '^.*$')); + if (!$n{from}) { + 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 @@ -2339,9 +2390,16 @@ sub process_args { # SMTP helo/ehlo $n{helo} = $o->{mail_helo} || interact("Helo: ", '^.*$') if (defined($o->{mail_helo})); - $n{helo} ||= ($hostname || ($G::quit_after eq 'connect' || $G::drop_after eq 'connect' || $G::drop_after_send eq 'connect') - ? $hostname - : interact("Helo: ", '^.*$')); + if (!$n{helo}) { + 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); + } + } # SMTP server and rcpt to are interdependant, so they are handled together $G::link{server} = $o->{mail_server} || interact("Server: ", '^.*$') @@ -2369,8 +2427,19 @@ sub process_args { $G::link{force_ipv4} = 1; } - $n{copy_routing} = $o->{copy_routing}; # only used in --dump output - $G::link{server} ||= get_server($o->{copy_routing} ? $o->{copy_routing} : $n{to}); + if (defined($o->{copy_routing})) { + if ($o->{copy_routing}) { + $n{copy_routing} = $o->{copy_routing}; # only used in --dump output + $G::link{server} ||= get_server($o->{copy_routing}); + } + else { + ptrans(12, "Option --copy-routing requires an argument"); + exit 1; + } + } + else { + $G::link{server} ||= get_server($n{to}); + } if ($o->{force_ipv4} && $G::link{server} =~ m|:|) { ptrans(12, "Option -4 is set but server appears to be ipv6, cannot proceed"); @@ -2420,6 +2489,7 @@ sub process_args { my $body = 'This is a test mailing'; # default message body $body = 'DUMP_AS_BODY_HAS_BEEN_SET' if (defined($o->{dump_as_body})); my $bound = ""; + my $main_content_type = "multipart/mixed"; my $stdin = undef; if (defined($o->{body_822})) { # the --body option is the entire 822 body and trumps and other options @@ -2441,57 +2511,100 @@ sub process_args { # 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 $mime_type = '%SWAKS_DEFAULT_MIMETYTPE%'; my $next_name = undef(); - my @parts = ( { body => $body, type => 'text/plain' } ); + my %parts = ( body => [], rest => [] ); $bound = "----=_MIME_BOUNDARY_000_$$"; while (defined(my $t = shift(@{$o->{attach_822}}))) { - if ($t =~ m|^[^/]+/[^/]+$| && !stat($t)) { - $mime_type = $t; - } elsif ($t =~ /^FILENAME:(.*)$/) { + if ($t =~ /^SWAKS_ATTACH-TYPE:(.*)$/) { + $mime_type = $1; + } elsif ($t =~ /^SWAKS_ATTACH-NAME:(.*)$/) { $next_name = $1; - } else { - push(@parts, { body => "$t", type => $mime_type }); + } elsif ($t =~ /^SWAKS_ATTACH-BODY:(.*)$/) { + if ($mime_type eq '%SWAKS_DEFAULT_MIMETYTPE%') { + $mime_type = 'text/plain'; + } + push(@{$parts{body}}, { body => $1, 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 ($t =~ /^SWAKS_ATTACH:(.*)$/) { + push(@{$parts{rest}}, { body => $1, type => $mime_type }); if (defined($next_name)) { - $parts[-1]{name} = $next_name; - $next_name = undef(); + $parts{rest}[-1]{name} = $next_name; + $next_name = undef(); } + } else { + ptrans(12, "Error processing attach args, unknown type when processing $t"); + exit(1); } } - $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}")) { - if (!exists($p->{name})) { - ($p->{name}) = $p->{body} =~ m|/?([^/]+)$|; + + # 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' }); + } + + # now loop through and reconcile stdin/file contents + foreach my $type ('body', 'rest') { + foreach my $part (@{$parts{$type}}) { + if ($part->{body} eq '-') { + if ($stdin) { + $part->{body} = $stdin; + } else { + $part->{body} = join('', ); + $stdin = $part->{body}; + } + } elsif (open(I, "<$part->{body}")) { + if (!exists($part->{name}) && $type ne 'body') { + ($part->{name}) = $part->{body} =~ m|/?([^/]+)$|; + } + $part->{body} = join('', ); + close(I); } - $p->{body} = join('', ); - close(I); } - $body .= "--$bound\n"; - if ($p->{type} =~ m|^text/plain$|i && !$p->{name}) { - $body .= "Content-Type: $p->{type}\n\n" . $p->{body} . "\n"; - } else { - if ($p->{name}) { - $body .= "Content-Type: $p->{type}; name=\"$p->{name}\"\n" - . "Content-Description: $p->{name}\n" - . "Content-Disposition: attachment; filename=\"$p->{name}\"\n"; - } else { - $body .= "Content-Type: $p->{type}\n" - . "Content-Disposition: attachment\n"; + } + + $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'; + } + + 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_$$"; + + $body .= "--$bound\n" + . 'Content-Type: multipart/alternative; boundary="' . $mp_bound . '"' . "\n\n"; + + foreach my $part (@{$parts{body}}) { + $body .= encode_mime_part($part, $mp_bound, 1); } - $body .= "Content-Transfer-Encoding: BASE64\n" - . "\n" . eb64($p->{body}, "\n") . "\n"; + $body .= "--$mp_bound--\n"; + } + else { + $body .= encode_mime_part($parts{body}[0], $bound, 1); + } + + # 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; # SMTP DATA # a '-' arg to -d is the same as setting -g @@ -2505,14 +2618,19 @@ sub process_args { $n{data} ||= '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: multipart/mixed; boundary="'.$bound.'"\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'; # The -g option trumps all other methods of getting the data if ($o->{data_on_stdin}) { $n{data} = join('', ); - } elsif ($n{data} !~ m/(\\n|\n)/ && open(F, "<$n{data}")) { + } elsif ($n{data} !~ m/\\n|\n|%NEWLINE%/) { + # if it doesn't have any newlines assume it's a file. Fail if we can't open. + if (!open(F, "<$n{data}")) { + ptrans(12, "data option appears to be a file but is not openable: $n{data} ($!)"); + exit(1); + } $n{data} = join('', ); close(F); } @@ -2537,13 +2655,25 @@ sub process_args { # 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[^:]+):/) { + 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 { - # continuation line (a header line that starts with a space continues the previous header) - push(@{$headers[-1]}, $headerLine); + # malformed header - no colon. Allow it anyway, we're a test tool + push(@headers, [ $headerLine ]); + $headers{$headerLine} = $headers[-1]; } } @@ -2586,15 +2716,12 @@ sub process_args { # 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/\r\n/g; + $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; - if ($n{data} =~ /%DATE%/) { - my $date_string = get_date_string(); # May be multiple replacements, but we only want to generate the string once - $n{data} =~ s/%DATE%/$date_string/g; - } + $n{data} =~ s/%DATE%/get_date_string()/ge; $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 @@ -2689,10 +2816,12 @@ sub process_args { # 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+$/); + 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 { @@ -2763,7 +2892,15 @@ sub process_args { $G::proxy{attr}{$attr} = $o->{"proxy_" . $attr} || interact("PROXY $attr: ", '^.+$'); } } - $G::proxy{version} ||= 1; + 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'; @@ -2787,6 +2924,10 @@ sub process_args { 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 @@ -2822,18 +2963,24 @@ sub process_args { "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 (/^([^=]+)=(.+)$/) { + 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*/, $o->{auth_extra})) { - my($keyword,$value) = split(/=/, $_, 2); - $keyword = uc($keyword); - $G::auth_extras{$keyword} = $value; + 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}) { @@ -2932,6 +3079,36 @@ sub process_args { 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; @@ -2980,8 +3157,7 @@ sub get_running_state { if ($dump_args->{'OUTPUT'} || $dump_args->{'ALL'}) { push(@parts, [ 'Output Info:', - ' show_time_lapse = ' . ($G::show_time_lapse ? 'TRUE' : 'FALSE'), - ' show_time_hires = ' . ($G::show_time_hires ? 'TRUE' : 'FALSE'), + ' 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'), @@ -2989,6 +3165,7 @@ sub get_running_state { ' 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'), @@ -3237,7 +3414,7 @@ sub build_version { } sub ext_usage { - if ($ARGV[0] =~ /^--help$/i) { + if ($O{help}) { require Config; $ENV{PATH} .= ":" unless $ENV{PATH} eq ""; $ENV{PATH} = "$ENV{PATH}$Config::Config{'installscript'}"; @@ -3245,7 +3422,7 @@ sub ext_usage { exec("perldoc", $0) || exit(1); # make parser happy %Config::Config = (); - } elsif ($ARGV[0] =~ /^--version$/i) { + } elsif ($O{version}) { print "$p_name version $p_version\n\n$p_cp\n"; } else { return; @@ -3369,21 +3546,23 @@ To prevent potential confusion in this document a flag to Swaks is always referr Options can be given to Swaks in three ways. They can be specified in a configuration file, in environment variables, and on the command line. Depending on the specific option and whether an argument is given to it, Swaks may prompt the user for the argument. -When Swaks evaluates its options, it first looks for a configuration file (either in a default location or specified with --config). Then it evaluates any options in environment variables. Finally, it evaluates command line options. At each round of processing, any options set earlier will be overridden. Additionally, any option can be prefixed with "no-" to cause Swaks to forget that the variable had previously been set. This capability is necessary because many options treat defined-but-no-argument differently than not-defined. +When Swaks evaluates its options, it first looks for a configuration file (either in a default location or specified with --config). Then it evaluates any options in environment variables. Finally, it evaluates command line options. At each round of processing, any options set earlier will be overridden. Additionally, any option can be prefixed with "no-" to cause Swaks to forget that the variable had previously been set (either in an earlier round, or earlier in the same round). This capability is necessary because many options treat defined-but-no-argument differently than not-defined. + +As a general rule, if the same option is given multiple time, the last time it is given is the one that will be used. This applies to both intra-method (if "--from user1@example.com --from user2@example.com" is given, user2@example.com will be used) and inter-method (if "from user1@example.com" is given in a config file and "--from user2@example.com" is given on the command line, user2@example.com will be used) The exact mechanism and format for using each of the types is listed below. =over 4 -=item CONFIGURATION FILE +=item CONFIGURATION FILES A configuration file can be used to set commonly-used or abnormally verbose options. By default, Swaks looks in order for $SWAKS_HOME/.swaksrc, $HOME/.swaksrc, and $LOGDIR/.swaksrc. If one of those is found to exist (and --config has not been used) that file is used as the configuration file. -Additionally, a configuration file in a non-default location can be specified using --config. If this is set and not given an argument Swaks will not use any configuration file, including any default file. If --config points to a readable file, it is used as the configuration file, overriding any default that may exist. If it points to a non-readable file and error will be shown and Swaks will exit. +Additionally, a configuration file in a non-default location can be specified using --config. If this is set and not given an argument Swaks will not use any configuration file, including any default file. If --config points to a readable file, it is used as the configuration file, overriding any default that may exist. If it points to a non-readable file an error will be shown and Swaks will exit. A set of "portable" defaults can also be created by adding options to the end of the Swaks program file. As distributed, the last line of Swaks should be "__END__". Any lines added after __END__ will be treated as the contents of a configuration file. This allows a set of user preferences to be automatically copied from server to server in a single file. -If present and configuration files have not been explicitly turned off, the __END__ config is always read. Only one other configuration file will ever be used per single invocation of Swaks, even if multiple configuration files are specified. Specifying the --config option with no argument turns off the processing of both the __END__ config and any actual config files. +If configuration files have not been explicitly turned off, the __END__ config is always read. Only one other configuration file will ever be used per single invocation of Swaks, even if multiple configuration files are specified. If the __END__ config and another config are to be read, the __END__ config will be processed first. Specifying the --config option with no argument turns off the processing of both the __END__ config and any actual config files. In a configuration file lines beginning with a hash (#) are ignored. All other lines are assumed to be an option to Swaks, with the leading dash or dashes optional. Everything after a option line's first space is assumed to be the option's argument and is not shell processed. Therefore, quoting is usually unneeded and will be included literally in the argument. Here is an example of the contents of a configuration file: @@ -3395,6 +3574,16 @@ In a configuration file lines beginning with a hash (#) are ignored. All other # entire argument. h-From: "Fred Example" +Options specific to configuration file: + +=over 4 + +=item --config [/path/to/config] + +This option provides a path to a specific configuration file to be used. If specified with no argument, no automatically-found configuration file (via $HOME, etc, or __END__) will be processed. If the argument is a valid file, that file will be used as the configuration file (after __END__ config). If option is not a valid, readable file, Swaks will error and exit. This option can be specified multiple times, but only the first time it is specified (in environment variable and the command line search order) will be used. + +=back + =item ENVIRONMENT VARIABLES Options can be supplied via environment variables. The variables are in the form $SWAKS_OPT_name, where name is the name of the option that would be specified on the command line. Because dashes aren't allowed in environment variable names in most UNIX-ish shells, no leading dashes should be used and any dashes inside the option's name should be replaced with underscores. The following would create the same options shown in the configuration file example: @@ -3404,6 +3593,8 @@ Options can be supplied via environment variables. The variables are in the for Setting a variable to an empty value is the same as specifying it on the command line with no argument. For instance, setting SWAKS_OPT_server="" would cause Swaks to prompt the use for the server to which to connect at each invocation. +Because there is no inherent order in options provided by setting environment variables, the options are sorted before being processed. This is not a great solution, but it at least defines the behavior, which would be otherwise undefined. As an example, if both SWAKS_OPT_from and SWAKS_OPT_f were set, the value from SWAKS_OPT_from would be used, because it sorts after SWAKS_OPT_f. Also as a result of not having an inherent order in environment processing, unsetting options with the "no-" prefix is unreliable. It works if the option being turned off sorts before "no-", but fails if it sorts after. Because "no-" is primarily meant to operate between config types (for instance, unsetting from the command line an option that was set in a config file), this is not likely to be a problem. + In addition to setting the equivalent of command line options, SWAKS_HOME can be set to a directory containing the default .swaksrc to be used. =item COMMAND LINE OPTIONS @@ -3438,7 +3629,7 @@ Explicitly tell Swaks to use network sockets and specify the hostname or IP addr =item -p, --port [port] -Specify which TCP port on the target is to be used, or prompt if no argument is listed. The argument can be a service name (as retrieved by getservbyname(3)) or a port number. The default port is determined by the --protocol option. See --protocol for more details. +Specify which TCP port on the target is to be used, or prompt if no argument is listed. The argument can be a service name (as retrieved by getservbyname(3)) or a port number. The default port is smtp/25 unless influenced by the --protocol or --tls-on-connect options. =item -li, --local-interface [IP or hostname[:port]] @@ -3500,11 +3691,11 @@ There is no default value for this option. If no recipients are provided via an =item -f, --from [email-address] -Use argument as envelope-sender for email, or prompt user if no argument specified. The string EE can be supplied to mean the null sender. If user does not specify a sender address a default value is used. The domain-part of the default sender is a best guess at the fully-qualified domain name of the local host. The method of determining the local-part varies. On Windows, Win32::LoginName() is used. On UNIX-ish platforms, the $LOGNAME environment variable is used if it is set. Otherwise getpwuid(3) is used. See also --force-getpwuid. +Use argument as envelope-sender for email, or prompt user if no argument specified. The string EE can be supplied to mean the null sender. If user does not specify a sender address a default value is used. The domain-part of the default sender is a best guess at the fully-qualified domain name of the local host. The method of determining the local-part varies. On Windows, Win32::LoginName() is used. On UNIX-ish platforms, the $LOGNAME environment variable is used if it is set. Otherwise getpwuid(3) is used. See also --force-getpwuid. If Swaks cannot determine a local hostname and the sender address is needed for the transaction, Swaks will error and exit. In this case, a valid string must be provided via this option. =item --ehlo, --lhlo, -h, --helo [helo-string] -String to use as argument to HELO/EHLO/LHLO command, or prompt use if no argument is specified. If this option is not used a best guess at the fully-qualified domain name of the local host is used. If the Sys::Hostname module, which is part of the base distribution, is not available the user will be prompted for a HELO value. Note that Sys::Hostname has been observed to not be able to find the local hostname in certain circumstances. This has the same effect as if Sys::Hostname were unavailable. +String to use as argument to HELO/EHLO/LHLO command, or prompt user if no argument is specified. If this option is not used a best guess at the fully-qualified domain name of the local host is used. If Swaks cannot determine a local hostname and the helo string is needed for the transaction, Swaks will error and exit. In this case, a valid string must be provided via this option. =item -q, --quit, --quit-after [stop-point] @@ -3522,9 +3713,9 @@ In a STARTTLS (but not tls-on-connect) session, terminate the transaction after =item XCLIENT -Quit after XCLIENT is sent +Quit after XCLIENT is sent. -=item TLS +=item STARTTLS, TLS Quit the transaction immediately following TLS negotiation. Note that this happens in different places depending on whether STARTTLS or tls-on-connect are used. This always quits after the point where TLS would have been negotiated, regardless of whether it was attempted. @@ -3548,11 +3739,23 @@ Quit after RCPT TO: is sent. =item --da, --drop-after [stop-point] -The option is similar to --quit-after, but instead of trying to cleanly shut down the session it simply terminates the session. This option accepts the same stop-points as --quit-after. +The option is similar to --quit-after, but instead of trying to cleanly shut down the session it simply terminates the session. This option accepts the same stop-points as --quit-after and additionally accepts DATA and DOT, detailed below. + +=over 4 + +=item DATA + +Quit after DATA is sent. + +=item DOT + +Quit after the final '.' of the message is sent. + +=back =item --das, --drop-after-send [stop-point] -This option is similar to --drop-after, but instead of dropping the connection after reading a response to the stop-point, it drops the connection immediately after sending stop-point. +This option is similar to --drop-after, but instead of dropping the connection after reading a response to the stop-point, it drops the connection immediately after sending stop-point. It accepts the same stop-points as --drop-after. =item --timeout [time] @@ -3838,7 +4041,7 @@ This is the "free form" XCLIENT option. Whatever value is provided for XCLIENT_ The primary advantage to this over the more specific options above is that there is no XCLIENT syntax validation here. This allows you to send invalid XCLIENT to the target server for testing. Additionally, at least one MTA (Message Systems' Momentum, formerly ecelerity) implements XCLIENT without advertising supported attributes. The --xclient option allows you to skip the "supported attributes" check when communicating with this type of MTA (though see also --xclient-no-verify). -The --xclient option can be mixed freely with the --xclient-* options above. If "--xclient-addr 192.168.0.1 --xclient 'FOO=bar NAME=wind'" is given to Swaks, "XCLIENT ADDR=192.168.0.1 FOO=bar NAME=wind" will be sent to the target server. +The --xclient option can be mixed freely with the --xclient-* options above. The argument to --xclient will be sent in its own command group. For instance, if "--xclient-addr 192.168.0.1 --xclient-port 26 --xclient 'FOO=bar NAME=wind'" is given to Swaks, "XCLIENT ADDR=192.168.0.1 PORT=26" and "XCLIENT FOO=bar NAME=wind" will both be sent to the target server. =item --xclient-no-verify @@ -3910,15 +4113,23 @@ Specify the destination port of the proxied connection. =head1 DATA OPTIONS -These options pertain to the contents for the DATA portion of the SMTP transaction. +These options pertain to the contents for the DATA portion of the SMTP transaction. By default a very simple message is sent. If the --attach or --attach-body options are used, Swaks attempts to upgrade to a MIME message. =over 4 =item -d, --data [data-portion] -Use argument as the entire contents of DATA, or prompt user if no argument specified. If the argument '-' is provided the data will be read from STDIN. If any other argument is provided and it represents the name of an open-able file, the contents of the file will be used. Any other argument will be itself for the DATA contents. +Use argument as the entire contents of DATA. + +If no argument is provided, user will be prompted to supply value. -The value can be on one single line, with \n (ASCII 0x5c, 0x6e) representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. The default value for this option is "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n". +If the argument '-' is provided the data will be read from STDIN with no prompt (same as -g). + +If the argument does not contain any literal (0x0a) or representative (0x5c, 0x6e or %NEWLINE%) newline characters, it will be treated as a filename. If the file is open-able, the contents of the file will be used as the data portion. If the file cannot be opened, Swaks will error and exit. + +Any other argument will be used as the DATA contents. + +The value can be on one single line, with \n (ASCII 0x5c, 0x6e) representing where line breaks should be placed. Leading dots will be quoted. Closing dot is not required but is allowed. The default value for this option is "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n". Very basic token parsing is performed on the DATA portion. The following table shows the recognized tokens and their replacement values: @@ -3952,6 +4163,10 @@ Replaced with the contents of the --add-header option. If --add-header is not s Replaced with the value specified by the --body option. See --body for default. +=item %NEWLINE% + +Replaced with carriage return, newline (0x0d, 0x0a). This is identical to using '\n' (0x5c, 0x6e), but doesn't have the escaping concerns that the backslash can cause on the newline. + =back =item -dab, --dump-as-body [section[,section]] @@ -3966,19 +4181,23 @@ Cause --dump-as-body to include plaintext passwords. This option is not recomme Specify the body of the email. The default is "This is a test mailing". If no argument to --body is given, prompt to supply one interactively. If '-' is supplied, the body will be read from standard input. If any other text is provided and the text represents an open-able file, the content of that file is used as the body. If it does not represent an open-able file, the text itself is used as the body. -If the message is forced to MIME format (see --attach) the argument to this option will be included unencoded as the first MIME part. Its content-type will always be text/plain. +If the message is forced to MIME format (see --attach) "--body 'body text'" is the same as "--attach-type text/plain --attach-body 'body text'". See --attach-body for details on creating a multipart/alternative body. =item --attach [attachment-specification] 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 respect to STDIN, file contents, etc. --attach can be supplied multiple times to create multiple attachments. By default, each attachment is attached as an application/octet-stream file. See --attach-type for changing this behavior. -If a filename is specified, the MIME encoding will include that file name. See --attach-name for more detail on file naming. +If the contents of the attachment are provided via a file name, the MIME encoding will include that file name. See --attach-name for more detail on file naming. 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-body [body-specification] + +This is a variation on --attach that is specifically for the body part of the email. It behaves identically to --attach in that it takes the same arguments and forces the creation of a MIME message. However, it is different in that the argument will always be the first MIME part in the message, no matter where in option processing order it is encountered. Additionally, --attach-body options stack to allow creation of multipart/alternative bodies. For example, '--attach-type text/plain --attach "plain text body" --attach-type text/html --attach "html body"' would create a multipart/alternative message body. + =item --attach-type [mime-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. +By default, content that gets MIME attached to a message with the --attach option is encoded as application/octet-stream (except for the body, which is text/plain by default). --attach-type changes the mime type for every --attach option which follows it. It can be specified multiple times. The current MIME type gets reset to application/octet-stream between processing body parts and other parts. =item --attach-name [name] @@ -3986,7 +4205,7 @@ This option sets the filename that will be included in the MIME part created for =item -ah, --add-header [header] -This option allows headers to be added to the DATA. If %H is present in the DATA it is replaced with the argument to this option. If %H is not present, the argument is inserted between the first two consecutive newlines in the DATA (that is, it is inserted at the end of the existing headers). +This option allows headers to be added to the DATA. If %NEW_HEADERS% is present in the DATA it is replaced with the argument to this option. If %NEW_HEADERS% is not present, the argument is inserted between the first two consecutive newlines in the DATA (that is, it is inserted at the end of the existing headers). The option can either be specified multiple times or a single time with multiple headers separated 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. @@ -4142,11 +4361,11 @@ This option causes Swaks to print the results of option processing, immediately =item --help -Display this help information. +Display this help information and exit. =item --version -Display version information. +Display version information and exit. =back @@ -4168,6 +4387,24 @@ This program was almost exclusively developed against Exim mail servers. It has =back +=head1 ENVIRONMENT VARIABLES + +=over 4 + +=item LOGNAME + +If Swaks must create a sender address, $LOGNAME is used as the message local-part if it is set, and unless --force-getpwuid is used. + +=item SWAKS_HOME + +Used when searching for a .swaksrc configuration file. See OPTION PROCESSING -> CONFIGURATION FILES above. + +=item SWAKS_OPT_* + +Environment variable prefix used to specify Swaks options from environment variables. See OPTION PROCESSING -> ENVIRONMENT VARIABLES above. + +=back + =head1 EXIT CODES =over 4 diff --git a/doc/index.html b/doc/index.html index d04114eb..e8c1540f 100644 --- a/doc/index.html +++ b/doc/index.html @@ -34,9 +34,9 @@ --> About

-Swaks is a featureful, flexible, scriptable, transaction-oriented SMTP test tool written and maintained by John Jetmore. It is free to use and licensed under the GNU GPLv2. Features include: +Swaks is a featureful, flexible, scriptable, transaction-oriented SMTP test tool written and maintained by John Jetmore. It is free to use and licensed under the GNU GPLv2. Features include:

    -
  • SMTP extensions including TLS, authentication, pipelining, and XCLIENT
  • +
  • SMTP extensions including TLS, authentication, pipelining, PROXY, PRDR, and XCLIENT
  • Protocols including SMTP, ESMTP, and LMTP
  • Transports including UNIX-domain sockets, internet-domain sockets (IPv4 and IPv6), and pipes to spawned processes
  • Completely scriptable configuration, with option specification via environment variables, configuration files, and command line
  • @@ -44,7 +44,7 @@


    Download

    -The latest version of Swaks is 20181104.0, which can be downloaded as a package or a standalone script. +The latest version of Swaks is 20190914.0, which can be downloaded as a package or a standalone script.

    There is also a versions page which lists every released version of Swaks, complete with changelogs and download links.


    @@ -52,15 +52,19 @@

    The reference documentation from the latest release, which includes quick-start examples, is available. There is also an Occasionally Asked Questions document.


    -News (rss)Source +

    +The Swaks source code is available at https://github.com/jetmore/swaks. +


    +News (rss)


    @@ -69,9 +73,10 @@

    • Send a mail to receive email when new versions are released
    • -
    • Follow @SwaksSMTP on twitter
    • +
    • Follow @SwaksSMTP on twitter
    • Contact the author - suggestion, tips, patches, feedback, critiques always welcome
    • -
    • Blog - Swaks-specific blog category (same feed as News section above)
    • +
    • Blog - Swaks-specific blog category (same feed as News section above)
    • +
    • Issues - Open an issue for feature requests and bugs
    diff --git a/doc/versions.html b/doc/versions.html index 1e14e622..7d2bd47f 100755 --- a/doc/versions.html +++ b/doc/versions.html @@ -42,6 +42,120 @@ + + + +
    20190914.0 +Links: Distribution, script only, version docs +

    +Change Summary:
    +

    +New Features:
    +  * Source is now available on github.com/jetmore/swaks
    +  * Added --body-attach option to allow more granularity in setting body
    +    information
    +  * Added 'data' and 'dot' as valid --drop-after-send and
    +    --drop-after arguments
    +  * Added %NEWLINE% as a new --data token
    +Notable Changes:
    +  * Options provided via environment variable are now sorted before
    +    processing to provide a deterministic processing order
    +  * Option bundling is no longer enabled.  This fixes several option
    +    processing oddities, like "-foobar" being interpreted as
    +    "-f oobar"
    +  * If the arg to --data looks like a file but is not openable, error
    +    and exit instead of using it the file name as the raw data value
    +  * Remove interactive prompts for --helo and --from when hostname cannot
    +    be determined internally, just error  and exit instead. If the user
    +    was not expecting an interactive experience, don't start one
    +  * Remove re-prompting for port when an invalid service name was supplied,
    +    just error and exit instead.  If the user was not expecting an
    +    interactive experience, don't start one
    +Notable Bugs Fixed:
    +  * Handle malformed headers more gracefully in header replacement
    +  * Fix bug causing the processing of options  prefixed with the negating
    +    "no-" to work unreliably
    +  * --version and --help should work even if they aren't the very
    +    first option
    +  * -S is now a distinct option from -s, as documented
    +  * Fix bug preventing the --option=arg option format from being
    +    unusable with --header and --attach* options
    +
    +Changelog:
    +
    +* 20181110 --add-header documentation was still referencing a single-char,
    +           no longer valid, replacement token. Replace with the correct token.
    +* 20181110 Doc fix for default body - %SWAKS_VERSION% missing trailing char.
    +* 20181110 Fix issue with malformed headers.  Don't fall over if header
    +           doesn't contain a colon or looks like an illegal continuation.
    +* 20181110 Fix --attach* option processing to remove possibly ambiguity
    +* 20181110 Implement --body-attach option to allow more granularity in
    +           setting body information (different mime types, alternatives, etc).
    +* 20181201 Config file fixes around searching default $SWAKS_HOME, $HOME,
    +           and $LOGDIR locations:
    +             - Searching default locations for the first existing
    +               PATH/.swaksrc did not actually work as documented.
    +             - If none of the default search environment variables was set,
    +               Swaks would not process the "portable" defaults optionally
    +               stored in the actual swaks script.
    +* 20181202 Since there is no inherent order to options provided in environment
    +           variables, sort them before processing to define an order.
    +* 20181202 Document the general rule that when processing duplicate options,
    +           the last option specified wins, both inter- and intra-method.
    +* 20181202 Document the unreliability of using environment variables to unset
    +           other environment variable options with the "no-" prefix.
    +* 20181203 Fix bug causing in "no-" option processing to work unreliably.
    +* 20181204 Tidying and clarifying the OPTION PROCESSING section of the docs.
    +* 20181204 Adding an ENVIRONMENT VARIABLES section to the doc.
    +* 20181208 When processing config file options with no leading '-' and any
    +           environment variable config, prefix the option with '--' for
    +           processing, not '-'. Bandaid for very minor difference between
    +           '-' and '--' option processing which I hope to fix soon.
    +* 20181210 --version and --help should work even if they aren't the very
    +           first option.
    +* 20181210 Add a flag for --dump-mail in the OUTPUT section of --dump
    +* 20181210 Turn on case-sensitivity for configuration options.  Needed to
    +           make -S distinct from -s, as documented.
    +* 20181210 Turn off option bundling. No practical use and it could cause
    +           real confusion (with bundling turned on, -foobar was "-f oobar"
    +           instead of an unknown option.
    +* 20181225 Add validation to --proxy-family (when proxy-version=1) and
    +           --proxy-version options.
    +* 20181225 --copy-routing should error when no argument given.
    +* 20190105 Update copyright year to 2019
    +* 20190105 Add documentation for missing --quit-after synonym STARTTLS
    +* 20190216 Adding data and dot as valid --drop-after-send and --drop-after
    +           arguments
    +* 20190618 Typo in documentation for --ehlo, reported by Konstantin Stephan
    +* 20190710 Clarify how XCLIENT arguments are grouped in --xclient doc
    +* 20190712 Enforce key=value format for arguments to --auth-extra and
    +           --auth-map
    +* 20190713 Small code tidy around %DATE% token replacement
    +* 20190713 Add %NEWLINE% as a new --data token
    +* 20190713 If the arg to --data looks like a file but is not openable,
    +           error and exit instead of using it the file name as the raw
    +           data value
    +* 20190713 --attach option processing was calling die() instead of
    +           ptrans/exit on error
    +* 20190713 Fix handling of --option=arg option format which prevented it
    +           from being used with --header and --attach* options
    +* 20190814 --tls-optional-strict was incorrectly marked internally as
    +           optionally accepting an argument
    +* 20190814 --use-old-data-tokens was not completely removed, clean up
    +* 20190815 Updating copyright year to 2019
    +* 20190815 --protocol's argument was incorrectly marked as optional
    +* 20180816 Rework how the --show-time-lapse option is tracked internally
    +           and displayed in --dump output
    +* 20190817 Rearrange internal option definition structure in preparation
    +           for major rework
    +* 20190817 Remove interactive prompts for helo and from when hostname
    +           cannot be determined internally.  Just error instead.
    +* 20190817 Cleaning up error messages that contained extra newlines
    +* 20190817 Remove re-prompting for port when an invalid service name was
    +           supplied. Just error and exit instead
    +> 20190914 released 20190914.0
    +
    +

    v20181104.0 Links: Distribution, script only, version docs