3 # Utility to convert an exim message-id to a human readable form
5 # https://bugs.exim.org/show_bug.cgi?id=2956
6 # Written by Andrew C Aitchison
8 # Copyright (c) 2023 The Exim Maintainers 2023
9 # SPDX-License-Identifier: GPL-2.0-or-later
11 # Portions taken from exicyclog.src, which is
12 # Copyright (c) University of Cambridge, 1995 - 2015
13 # See the file NOTICE for conditions of use and distribution.
15 # https://bugs.exim.org/show_bug.cgi?id=2956
16 # https://exim.org/exim-html-current/doc/html/spec_html/ch-how_exim_receives_and_delivers_mail.html#SECTmessiden
18 # Except when they appear in comments, the following placeholders in this
19 # source are replaced when it is turned into a runnable script:
25 # EXIM_RELEASE_VERSION
26 # EXIM_VARIANT_VERSION
35 use constant { TRUE => 1, FALSE => 0 };
37 if (defined $ENV{TZ}) {
38 my $zonefile = "/usr/share/zoneinfo/$ENV{TZ}";
39 if (defined $ENV{TZDIR}) {
41 $zonefile="$ENV{TZDIR}/$ENV{TZ}";
43 warn "No directory TZDIR=$ENV{TZDIR}\n"
46 warn "Cannot read timezone file $zonefile (from TZDIR/TZ)\n\t'man tzset' may help.\n"
50 my $localhost_number; # An Exim config value
52 my $p_name = basename $0;
53 my $p_version = "20230203.0";
55 Copyright (c) 2023 The Exim Maintainers 2023
57 Portions taken from exicyclog.src, which is
58 Copyright (c) University of Cambridge, 1995 - 2015
59 See the file NOTICE for conditions of use and distribution.
62 $ENV{PATH} = "/bin:/usr/bin:/usr/sbin";
64 use POSIX qw(strftime);
66 sub main::VERSION_MESSAGE()
68 print basename($0), ": $0\n";
69 print "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
70 print "perl( runtime): $]\n";
74 $optbase, $optbase36, $optbase62,
75 $optunix, $optgmt, $optlocal,
77 $opteximpath,$optconfigfile);
79 # Cannot use $debug here, since we haven't read ARGV yet.
81 warn join(" ", $0, @ARGV), "\n";
84 # Case is ignored, abbreviations are allowed.
86 # Allow windows style arguments /...
87 # "--|-|\+|\/" => \$prefix_pattern,
88 # "--|\/" => \$long_prefix_pattern,
91 "base=i" => \$optbase,
93 "base36" => \$optbase36,
95 "base62" => \$optbase62,
97 "localhost_number=s" => \$localhost_number, # cf "local"
104 "local" => \$optlocal, # cf "localhost_number"
105 "l" => \$optlocal, # cf "localhost_number"
109 # exim args given by the test harness
110 "C=s" => \$optconfigfile,
111 "dexim_path=s" => \$opteximpath,
114 "nodebug" => \$nodebug,
115 "no-debug" => \$nodebug,
117 'help' => sub { pod2usage(-exit => 0) },
122 -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
126 # die("Error in command line arguments\n");
128 $debug = undef if $nodebug;
132 warn "$0 ", join(" ", @ARGV), "\n";
133 warn "C=$optconfigfile\n" if defined $optconfigfile;
134 warn "dexim_path=$opteximpath\n" if defined $opteximpath;
137 unless ($optgmt || $optunix || $optlocal) {
141 if (defined($optbase36) && defined($optbase62)) {
142 die "cannot be base36 and base62\n";
145 if (defined $optbase36) {
148 if (defined $optbase62) {
151 if (defined $optbase) {
152 if ($optbase =~ 62) {
154 } elsif ($optbase =~ 36) {
157 warn "\toptbase36=$optbase36\n" if defined $optbase36;
158 warn "\toptbase62=$optbase62\n"if defined $optbase62;
159 die "unknown base option $optbase\n";
163 # Some Operating Systems have case-insensitive file systems
164 # (at least by default).
165 # This limits the characters available for the message-id
166 # and hence the base Exim uses to encode numbers.
168 # We use Perl's idea of the operating system.
169 # Should we instead use the script "scripts/os-type" which comes with Exim ?
171 if ($^O =~ /darwin|cygwin/i) { # darwin aka MacOS X
177 if ("BASE_62" != $defaultbase and !defined $optbase) {
178 die "base_62 mismatch: OS implies $defaultbase but config has BASE_62\n";
181 my $base=$defaultbase;
182 $base = $optbase if $optbase;
185 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
186 my $base36_chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
189 $base_chars = $base62_chars;
191 $base_chars = $base36_chars;
194 # We use this to decode both base62 and base36
196 #warn "decode62(", join(",", @_), ")\n";
198 unless ($text =~ /^[$base_chars]+$/) {
199 die "$text is not base $base\n";
202 foreach my $tt (split //, $text) {
203 $n = $n * $base + index($base_chars, $tt);
205 #warn "$text -> $n\n";
209 sub get_configfilename()
211 if (defined $optconfigfile) {
212 if ( -r $optconfigfile ) {
213 warn "using config $optconfigfile\n" if $debug;
214 return $optconfigfile;
216 die "cannot read $optconfigfile\n";
220 # See if this installation is using the esoteric "USE_EUID" feature of
221 # Exim, in which it uses the effective user id as a suffix for the
222 # configuration file name. In order for this to work, exim_msgdate
223 # must be run under the appropriate euid.
225 if ("CONFIGURE_FILE_USE_EUID" eq "yes" ) {
229 # See if this installation is using the esoteric "USE_NODE"
230 # feature of Exim, in which it uses the host's name as a suffix
231 # for the configuration file name.
233 if ("CONFIGURE_FILE_USE_NODE" eq "yes") {
234 $hostsuffix=`uname -n`;
237 # Now find the configuration file name.
238 # This has got complicated because the CONFIGURE_FILE value may now
239 # be a list of files. The one that is used is the first one that
240 # exists. Mimic the code in readconf.c by testing first for the
241 # suffixed file in each case.
245 foreach $baseconfig (split /:/, "CONFIGURE_FILE") {
247 if (-f "$baseconfig$euid$hostsuffix" ) {
248 $config="$baseconfig$euid$hostsuffix";
249 } elsif (-f "$baseconfig$euid" ) {
250 $config="$baseconfig$euid";
251 } elsif (-f "$baseconfig$hostsuffix" ) {
252 $config="$baseconfig$hostsuffix";
253 } elsif (-f "$baseconfig" ) {
254 $config="$baseconfig";
259 die "No config file found\n";
263 } # sub get_configfilename
267 warn "before reading configfiles:\n";
268 if (defined $localhost_number) {
269 warn "localhost_number=$localhost_number\n";
271 warn "localhost_number unset\n";
275 if (defined $localhost_number) {
276 if ($localhost_number eq "none") {
277 $localhost_number = undef;
280 my $config = get_configfilename();
281 warn "Reading config $config to find localhost_number\n" if $debug;
284 # This does not do any expansions or lookups,
285 # so could be end up with a different value for localhost_number
286 # from the one that exim finds.
287 open(CONFIG, "<", $config) or
288 die "cannot open config $config :$!\n";
291 if (/^\s*localhost_number\s*=\s*(\d+)\s*$/) {
292 $localhost_number = $1;
295 close CONFIG or die "cannot close config $config: $!\n";
296 warn "$config gives localhost_number $localhost_number\n"
297 if $debug and defined $localhost_number;
299 # This way we get the expanded value for localhost_number
300 # directly from exim, but we have to guess which exim binary ...
301 # On Debian and Ubuntu, /usr/sbin/exim is a link to exim4 so is OK.
303 # Even if given on command line, we cannot use $opteximpath
304 # since it is the full path to this script,
305 # or $config since it is tainted.
307 warn "running system exim -bP localhost_number\n" if $debug;
308 my $exim_bP_localhost_number = `/usr/sbin/exim -bP localhost_number`;
309 if ($exim_bP_localhost_number =~ /^localhost_number\s*=\s*(\d*)/) {
310 $localhost_number = $1;
312 warn "exim_bP_localhost_number $exim_bP_localhost_number gives localhost_number $localhost_number\n"
313 if $debug and defined $localhost_number;
317 if (defined $localhost_number) {
318 die "localhost_number > 16\n"
319 if $localhost_number > 16;
320 die "localhost_number > 10\n"
321 if $localhost_number > 10 && ($base != 62);
325 if (defined $localhost_number) {
326 warn "localhost_number=$localhost_number\n";
328 warn "localhost_number unset\n";
332 sub unpack_time($$) {
333 my ($seconds, $fractions) = @_;
334 # warn "encoded: seconds: $seconds fractions: $fractions\n";
335 $seconds = decode62($seconds);
336 $fractions = decode62($fractions) if $fractions;
338 if (defined $localhost_number && $localhost_number ne "none") {
339 print "localhost_number $localhost_number\n" if $debug;
341 # MacOS/Darwin and Cygwin
342 $id_resolution = 100;
345 $id_resolution = 200;
347 $fractions -= $localhost_number * $id_resolution;
350 # MacOS/Darwin and Cygwin
351 $id_resolution = 1000;
354 $id_resolution = 2000;
357 while ($fractions > $id_resolution) {
359 $fractions -= $id_resolution;
361 while ($fractions < -1e-7) {
363 $fractions += $id_resolution;
365 # $seconds += $fractions / $id_resolution;
367 # warn "decoded: seconds: $seconds, fractions: $fractions/$id_resolution\n";
369 return ($seconds, $fractions / $id_resolution);
370 } # sub unpack_time($$)
372 sub print_time($$$$$$)
374 my ($seconds, $decimal, $unix, $zulu, $localtm, $pid) = @_;
377 my $ounix = defined($unix) ? $unix : "undef";
378 my $ozulu = defined($zulu) ? $zulu : "undef";
379 my $olocal = defined($localtm) ? $localtm : "undef";
380 my $opid = defined($pid) ? $pid : "undef";
381 warn "print_time($seconds, $decimal, $ounix, $ozulu, $olocal, $opid)\n"
385 $pidstring = "\tpid $pid" if defined $pid;
387 my $decimalstring = "";
390 $decimalstring = sprintf(".%6.6d", 1000000*$decimal);
393 unless (defined $unix or defined $zulu or defined $localtm) {
394 warn "No time type requested. Reporting UNIX time\n";
398 $secondsstring = $seconds;
399 print "$secondsstring$decimalstring$pidstring\n";
402 $secondsstring = strftime("%F %T", gmtime($seconds));
403 print "$secondsstring$decimalstring$pidstring\n";
405 if (defined $localtm) {
406 $secondsstring = strftime("%F %T%%s %Z%%s\n", localtime($seconds));
407 # print "secondstring $secondsstring\n" if $debug;
408 printf($secondsstring, $decimalstring, $pidstring);
411 } # sub print_time($$$$$$)
413 foreach my $msgid (@ARGV) {
414 my ($seconds, $pid, $fractions, $decimal);
417 /(^|[\s<])E?([a-zA-Z0-9]{6})-([a-zA-Z0-9]{6})-([a-zA-Z0-9]{2})/)
419 # Should take either the log form of timestamp,
420 # the Message-ID: header form with the leading 'E', ...
421 ($seconds, $pid, $fractions) = ($2, $3, $4);
422 ($seconds, $decimal) = unpack_time($seconds, $fractions);
423 $pid = decode62($pid);
424 #warn "$seconds, $pid, $fractions\n";
425 } elsif ($msgid =~ /(^|[^0-9A-Za-z])([a-zA-Z0-9]{6})$/) {
426 # ... or just the timecode section before the first '-'
427 ($seconds, $pid, $decimal) = (decode62($2), undef, 0);
429 warn "$msgid not parsed\n";
434 print "msgid: $msgid\n";
435 my $ogmt = defined($optgmt) ? $optgmt : "undef";
436 my $ounix = defined($optunix) ? $optunix : "undef";
437 my $olocal = defined($optlocal) ? $optlocal : "undef";
438 my $opid = defined($optpid) ? $optpid : "undef";
439 print "print_time($seconds, $decimal, $ounix, $ogmt, $olocal, $opid)\n";
441 $pid = undef unless $optpid;
442 print_time($seconds, $decimal, $optunix, $optgmt, $optlocal, $pid);
447 exim_msgdate - Utility to convert an exim message-id to a human readable date+time
451 B<exim_msgdate> [ -u|--unix | --GMT | --z|-Zulu | --UTC | -l|--local ]
452 [ --base 36 | --base 62 | --base36 | --base62 | --b36 | --b62 ]
453 [ --pid ] [ --debug ] [ --localhost_number ]
454 [ -c c<full path to exim cnfig file> ]
455 exim-message-id [ | exim-message-id ...]
457 B<exim_msgdate> --help|--man
461 B<exim_msgdate> is a tool which converts an exim message-id to a human
462 readable form, usuall just the date+time, but with the I<--pid> option
463 the process id as well.
467 Three exim message ID formats are recognized.
468 In each case the 'X's are taken from the base (see below) which depends upon the platform.
472 =item XXXXXX-XXXXXX-XX
474 found in the exim logfile,
476 =item EXXXXXX-XXXXXX-XX
478 found in the Message-Id header,
482 just the first six characters of the message id.
488 =head2 Time Zones and Unix Time
494 Display time as seconds since 1 Jan 1970, the Unix Epoch.
496 =item B<--GMT> B<-u|--UTC> B<-z|--zulu>
498 Display time in GMT/UTC - we assume these are the same.
499 Zulu time is another name for GMT.
501 =item B<-l | --local>
503 Display time in the local time-zone.
505 Do not confuse this with the L<--localhost_number|/--localhost_number-n> option.
509 The default is the local timezone.
511 =head2 User Assistance Options
517 A brief list of the options
521 A more detailed manual for B<exim_msgdate>
525 Information about what went wrong, mostly for developers.
529 =head2 Specialized Options
533 =item B<--base> n | B<--base36> | B<--base62>
535 The message-id is usually encoded in base-62 (0-9A-Za-z),
536 but on systems with case-insensitive file systems, such as MacOS and Cygwin,
537 base-36 (0-9A-Z) is used instead.
538 The installation script should have set the default appropriately,
539 but these options allow the default base to be overridden.
541 The default matches C<exim>; in this installation it is base-BASE_62.
545 Report the process id as well as the date and time in the message-id.
547 =item B<--localhost_number> n
549 If the Exim configuration option B<localhost_number> has been set,
550 the third and final section of the message-id will include this and
551 the timer resolution will change (see the Exim Spec. for details).
552 C<Exim_msgdate> reads the Exim config file (see L<--C|/C-full-path-to-exim-configuration-file>) to find this value,
553 but it can be overridden with this option.
555 The value is an integer between 0 and 16, or the value "none" which
556 means there is no localhost_number.
558 Do not confuse this with the L<--local|/l---local> option, which displays times
559 in the local timezone.
561 =item B<--C> B<full path to exim configuration file>
563 This overrides the usual exim search path.
564 We set C<localhost_number> from the exim configfile.
568 The test test harness passes the full path of the C<exim> binary,
569 or here the C<exim_msgdate> being tested. Not currently used.
577 L<Exim spec.txt chapter 4|https://exim.org/exim-html-current/doc/html/spec_html/ch-how_exim_receives_and_delivers_mail.html#SECTmessiden>