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
33 use if $^V >= v5.19.11, experimental => 'smartmatch';
40 use constant { TRUE => 1, FALSE => 0 };
42 if (defined $ENV{TZ}) {
43 my $zonefile = "/usr/share/zoneinfo/$ENV{TZ}";
44 if (defined $ENV{TZDIR}) {
46 $zonefile="$ENV{TZDIR}/$ENV{TZ}";
48 warn "No directory TZDIR=$ENV{TZDIR}\n"
51 warn "Cannot read timezone file $zonefile (from TZDIR/TZ)\n\t'man tzset' may help.\n"
55 my $localhost_number; # An Exim config value
56 my $nolocalhost_number;
58 my $p_name = basename $0;
59 my $p_version = "20230304.0";
61 Copyright (c) 2023 The Exim Maintainers 2023
63 Portions taken from exicyclog.src, which is
64 Copyright (c) University of Cambridge, 1995 - 2015
65 See the file NOTICE for conditions of use and distribution.
68 $ENV{PATH} = "/bin:/usr/bin:/usr/sbin";
70 use POSIX qw(strftime);
73 $optbase, $optbase36, $optbase62,
74 $optunix, $optgmt, $optlocal,
76 $opteximpath,$optconfigfile);
78 # Cannot use $debug here, since we haven't read ARGV yet.
80 warn join(" ", $0, @ARGV), "\n";
83 # Case is ignored, abbreviations are allowed.
85 # Allow windows style arguments /...
86 # "--|-|\+|\/" => \$prefix_pattern,
87 # "--|\/" => \$long_prefix_pattern,
90 "base=i" => \$optbase,
92 "base36" => \$optbase36,
94 "base62" => \$optbase62,
96 "localhost_number=s" => \$localhost_number, # cf "local"
97 "nolocalhost_number" => \$nolocalhost_number,
98 "no-localhost_number" => \$nolocalhost_number,
99 "no_localhost_number" => \$nolocalhost_number,
106 "local" => \$optlocal, # cf "localhost_number"
107 "l" => \$optlocal, # cf "localhost_number"
111 # exim args given by the test harness
112 "C=s" => \$optconfigfile,
113 "dexim_path=s" => \$opteximpath,
116 "nodebug" => \$nodebug,
117 "no-debug" => \$nodebug,
119 'help' => sub { pod2usage(-exit => 0) },
124 -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
128 print basename($0), ": $p_version $0\n";
129 print "exim build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
130 print "perl(runtime): $]\n";
134 # die("Error in command line arguments\n");
136 $debug = undef if $nodebug;
140 warn "$0 ", join(" ", @ARGV), "\n";
141 warn "C=$optconfigfile\n" if defined $optconfigfile;
142 warn "dexim_path=$opteximpath\n" if defined $opteximpath;
145 unless ($optgmt || $optunix || $optlocal) {
149 if (defined($optbase36) && defined($optbase62)) {
150 die "cannot be base36 and base62\n";
153 if (defined $optbase36) {
156 if (defined $optbase62) {
159 if (defined $optbase) {
160 if ($optbase =~ 62) {
162 } elsif ($optbase =~ 36) {
165 warn "\toptbase36=$optbase36\n" if defined $optbase36;
166 warn "\toptbase62=$optbase62\n"if defined $optbase62;
167 die "unknown base option $optbase\n";
171 # Some Operating Systems have case-insensitive file systems
172 # (at least by default).
173 # This limits the characters available for the message-id
174 # and hence the base Exim uses to encode numbers.
176 # We use Perl's idea of the operating system.
177 # Should we instead use the script "scripts/os-type" which comes with Exim ?
179 if ($^O =~ /darwin|cygwin/i) { # darwin aka MacOS X
185 if ("BASE_62" != $defaultbase and !defined $optbase) {
186 die "base_62 mismatch: OS implies $defaultbase but config has BASE_62\n";
189 my $base=$defaultbase;
190 $base = $optbase if $optbase;
193 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
194 my $base36_chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
197 $base_chars = $base62_chars;
199 $base_chars = $base36_chars;
202 # We use this to decode both base62 and base36
204 #warn "decode62(", join(",", @_), ")\n";
206 unless ($text =~ /^[$base_chars]+$/) {
207 die "$text is not base $base\n";
210 foreach my $tt (split //, $text) {
211 $n = $n * $base + index($base_chars, $tt);
213 #warn "$text -> $n\n";
217 sub get_configfilename()
219 if (defined $optconfigfile) {
220 if ( -r $optconfigfile ) {
221 warn "using config $optconfigfile\n" if $debug;
222 return $optconfigfile;
224 die "cannot read $optconfigfile\n";
228 # See if this installation is using the esoteric "USE_EUID" feature of
229 # Exim, in which it uses the effective user id as a suffix for the
230 # configuration file name. In order for this to work, exim_msgdate
231 # must be run under the appropriate euid.
233 if ("CONFIGURE_FILE_USE_EUID" eq "yes" ) {
237 # See if this installation is using the esoteric "USE_NODE"
238 # feature of Exim, in which it uses the host's name as a suffix
239 # for the configuration file name.
241 if ("CONFIGURE_FILE_USE_NODE" eq "yes") {
242 $hostsuffix=`uname -n`;
245 # Now find the configuration file name.
246 # This has got complicated because the CONFIGURE_FILE value may now
247 # be a list of files. The one that is used is the first one that
248 # exists. Mimic the code in readconf.c by testing first for the
249 # suffixed file in each case.
253 foreach $baseconfig (split /:/, "CONFIGURE_FILE") {
255 if (-f "$baseconfig$euid$hostsuffix" ) {
256 $config="$baseconfig$euid$hostsuffix";
257 } elsif (-f "$baseconfig$euid" ) {
258 $config="$baseconfig$euid";
259 } elsif (-f "$baseconfig$hostsuffix" ) {
260 $config="$baseconfig$hostsuffix";
261 } elsif (-f "$baseconfig" ) {
262 $config="$baseconfig";
267 die "No config file found\n";
271 } # sub get_configfilename
275 warn "before reading configfiles:\n";
276 if (defined $localhost_number) {
277 warn "localhost_number=$localhost_number\n";
279 warn "localhost_number unset\n";
281 if (defined $nolocalhost_number) {
282 warn "nolocalhost_number=$nolocalhost_number\n";
284 warn "nolocalhost_number unset\n";
288 if (defined $localhost_number) {
289 if ($localhost_number eq "none") {
290 $localhost_number = undef;
291 $nolocalhost_number = TRUE;
293 if ($nolocalhost_number) {
294 die "aborting: localhost_number and nolocalhost_number both set\n ";
296 $nolocalhost_number = FALSE;
300 unless (defined $nolocalhost_number) {
301 warn "Looking for config file\n" if $debug;
302 my $config = get_configfilename();
303 warn "Reading config $config to find localhost_number\n" if $debug;
306 # This does not do any expansions or lookups,
307 # so could be end up with a different value for localhost_number
308 # from the one that exim finds.
309 open(CONFIG, "<", $config) or
310 die "cannot open config $config :$!\n";
313 if (/^\s*localhost_number\s*=\s*(\d+)\s*$/) {
314 $localhost_number = $1;
317 close CONFIG or die "cannot close config $config: $!\n";
318 warn "$config gives localhost_number $localhost_number\n"
319 if $debug and defined $localhost_number;
322 warn "cannot read config file $config\n";
324 # This way we get the expanded value for localhost_number
325 # directly from exim, but we have to guess which exim binary ...
326 # On Debian and Ubuntu, /usr/sbin/exim is a link to exim4 so is OK.
328 # Even if given on command line, we cannot use $opteximpath
329 # since it is the full path to this script,
330 # or $config since it is tainted.
332 warn "running system exim -bP localhost_number\n" if $debug;
333 my $exim_bP_localhost_number = `/usr/sbin/exim -bP localhost_number`;
334 if ($exim_bP_localhost_number =~ /^localhost_number\s*=\s*(\d*)/) {
335 $localhost_number = $1;
337 warn "exim_bP_localhost_number $exim_bP_localhost_number gives localhost_number $localhost_number\n"
338 if $debug and defined $localhost_number;
342 if (defined $localhost_number) {
343 if ($localhost_number =~ /\D/) {
344 die "localhost_number must be a number >=0\n";
345 } elsif ($localhost_number =~ /^\d*$/) {
346 die "localhost_number > 16\n"
347 if $localhost_number > 16;
348 die "localhost_number > 10\n"
349 if $localhost_number > 10 && ($base != 62);
351 warn "clearing localhost_number - was $localhost_number\n";
352 undef $localhost_number;
353 $nolocalhost_number=TRUE;
358 if (defined $localhost_number) {
359 warn "localhost_number=$localhost_number\n";
361 warn "localhost_number unset\n";
365 sub unpack_time($$) {
366 my ($seconds, $fractions) = @_;
367 # warn "encoded: seconds: $seconds fractions: $fractions\n";
368 $seconds = decode62($seconds);
369 $fractions = decode62($fractions) if $fractions;
371 if (defined $localhost_number && $localhost_number ne "none") {
372 print "localhost_number $localhost_number\n" if $debug;
374 # MacOS/Darwin and Cygwin
375 $id_resolution = 100;
378 $id_resolution = 200;
380 $fractions -= $localhost_number * $id_resolution;
383 # MacOS/Darwin and Cygwin
384 $id_resolution = 1000;
387 $id_resolution = 2000;
390 while ($fractions > $id_resolution) {
392 $fractions -= $id_resolution;
394 while ($fractions < -1e-7) {
396 $fractions += $id_resolution;
398 # $seconds += $fractions / $id_resolution;
400 # warn "decoded: seconds: $seconds, fractions: $fractions/$id_resolution\n";
402 return ($seconds, $fractions / $id_resolution);
403 } # sub unpack_time($$)
405 sub print_time($$$$$$)
407 my ($seconds, $decimal, $unix, $zulu, $localtm, $pid) = @_;
410 my $ounix = defined($unix) ? $unix : "undef";
411 my $ozulu = defined($zulu) ? $zulu : "undef";
412 my $olocal = defined($localtm) ? $localtm : "undef";
413 my $opid = defined($pid) ? $pid : "undef";
414 warn "print_time($seconds, $decimal, $ounix, $ozulu, $olocal, $opid)\n"
418 $pidstring = "\tpid $pid" if defined $pid;
420 my $decimalstring = "";
423 $decimalstring = sprintf(".%6.6d", 1000000*$decimal);
426 unless (defined $unix or defined $zulu or defined $localtm) {
427 warn "No time type requested. Reporting UNIX time\n";
431 $secondsstring = $seconds;
432 print "$secondsstring$decimalstring$pidstring\n";
435 $secondsstring = strftime("%F %T", gmtime($seconds));
436 print "$secondsstring$decimalstring$pidstring\n";
438 if (defined $localtm) {
439 $secondsstring = strftime("%F %T%%s %Z%%s\n", localtime($seconds));
440 # print "secondstring $secondsstring\n" if $debug;
441 printf($secondsstring, $decimalstring, $pidstring);
444 } # sub print_time($$$$$$)
446 foreach my $msgid (@ARGV) {
447 my ($seconds, $pid, $fractions, $decimal);
450 /(^|[\s<])E?([a-zA-Z0-9]{6})-([a-zA-Z0-9]{6})-([a-zA-Z0-9]{2})/)
452 # Should take either the log form of timestamp,
453 # the Message-ID: header form with the leading 'E', ...
454 ($seconds, $pid, $fractions) = ($2, $3, $4);
455 ($seconds, $decimal) = unpack_time($seconds, $fractions);
456 $pid = decode62($pid);
457 #warn "$seconds, $pid, $fractions\n";
458 } elsif ($msgid =~ /(^|[^0-9A-Za-z])([a-zA-Z0-9]{6})$/) {
459 # ... or just the timecode section before the first '-'
460 ($seconds, $pid, $decimal) = (decode62($2), undef, 0);
462 warn "$msgid not parsed\n";
467 print "msgid: $msgid\n";
468 my $ogmt = defined($optgmt) ? $optgmt : "undef";
469 my $ounix = defined($optunix) ? $optunix : "undef";
470 my $olocal = defined($optlocal) ? $optlocal : "undef";
471 my $opid = defined($optpid) ? $optpid : "undef";
472 print "print_time($seconds, $decimal, $ounix, $ogmt, $olocal, $opid)\n";
474 $pid = undef unless $optpid;
475 print_time($seconds, $decimal, $optunix, $optgmt, $optlocal, $pid);
480 exim_msgdate - Utility to convert an exim message-id to a human readable date+time
484 B<exim_msgdate> [ -u|--unix | --GMT | --z|-Zulu | --UTC | -l|--local ]
485 [ --base 36 | --base 62 | --base36 | --base62 | --b36 | --b62 ]
486 [ --pid ] [ --debug ] [ --localhost_number ]
487 [ -c c<full path to exim cnfig file> ]
488 exim-message-id [ | exim-message-id ...]
490 B<exim_msgdate> --help|--man
494 B<exim_msgdate> is a tool which converts an exim message-id to a human
495 readable form, usuall just the date+time, but with the I<--pid> option
496 the process id as well.
500 Three exim message ID formats are recognized.
501 In each case the 'X's are taken from the base (see below) which depends upon the platform.
505 =item XXXXXX-XXXXXX-XX
507 found in the exim logfile,
509 =item EXXXXXX-XXXXXX-XX
511 found in the Message-Id header,
515 just the first six characters of the message id.
521 =head2 Time Zones and Unix Time
527 Display time as seconds since 1 Jan 1970, the Unix Epoch.
529 =item B<--GMT> B<-u|--UTC> B<-z|--zulu>
531 Display time in GMT/UTC - we assume these are the same.
532 Zulu time is another name for GMT.
534 =item B<-l | --local>
536 Display time in the local time-zone.
538 Do not confuse this with the L<--localhost_number|/--localhost_number-n> option.
542 The default is the local timezone.
544 =head2 User Assistance Options
550 A brief list of the options
554 A more detailed manual for B<exim_msgdate>
558 Information about what went wrong, mostly for developers.
562 =head2 Specialized Options
566 =item B<--base> n | B<--base36> | B<--base62>
568 The message-id is usually encoded in base-62 (0-9A-Za-z),
569 but on systems with case-insensitive file systems, such as MacOS and Cygwin,
570 base-36 (0-9A-Z) is used instead.
571 The installation script should have set the default appropriately,
572 but these options allow the default base to be overridden.
574 The default matches C<exim>; in this installation it is base-BASE_62.
578 Report the process id as well as the date and time in the message-id.
580 =item B<--localhost_number> n
582 If the Exim configuration option B<localhost_number> has been set,
583 the third and final section of the message-id will include this and
584 the timer resolution will change (see the Exim Spec. for details).
585 C<Exim_msgdate> reads the Exim config file (see L<--C|/C-full-path-to-exim-configuration-file>) to find this value,
586 but it can be overridden with this option.
588 The value is an integer between 0 and 16, or the value "none" which
589 means there is no localhost_number.
591 Do not confuse this with the L<--local|/l---local> option, which displays times
592 in the local timezone.
594 =item B<--C> B<full path to exim configuration file>
596 This overrides the usual exim search path.
597 We set C<localhost_number> from the exim configfile.
601 The test test harness passes the full path of the C<exim> binary,
602 or here the C<exim_msgdate> being tested. Not currently used.
610 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>