Manitou-Mail logo title

Source file: mdx/lib/Manitou/MailFormat.pm

# Copyright (C) 2004-2011 Daniel Verite

# This file is part of Manitou-Mail (see http://www.manitou-mail.org)

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

package Manitou::MailFormat;

use strict;
use vars qw(@ISA @EXPORT_OK);
use Encode;
use Text::Wrap;
use POSIX qw(strftime locale_h);
use MIME::Words qw(:all);

require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw(encode_header add_date_header encode_text_body parse_sender_date);

# Fixed version of encode_mimewords, that merges consecutive rfc2047
# encoded words separated by a space.
sub my_encode_mimewords {
    my ($rawstr, $in_charset) = @_;
    my $charset  = uc($in_charset) || 'US-ASCII';
    my $encoding = "Q";
    my $NONPRINT = "\\x00-\\x1F\\x7F-\\xFF";

    ### Encode any "words" with unsafe characters.
    ###    We limit such words to 18 characters, to guarantee that the
    ###    worst-case encoding give us no more than 54 + ~10 < 75 characters
    my $word;
    $rawstr =~ s{([a-zA-Z0-9\x7F-\xFF]{1,18})}{     ### get next "word"
	$word = $1;
	(($word !~ /[$NONPRINT]/o)
	 ? $word                                          ### no unsafe chars
	 : encode_mimeword($word, $encoding, $charset));  ### has unsafe chars
    }xeg;
    $rawstr =~ s/=\?$charset\?$encoding\?(.*)\?=\s+=\?$charset\?$encoding\?(.*)\?=/=?$charset?$encoding?$1_$2?=/g;
    $rawstr;
}

sub encode_header {
  my $top=shift;
  my $header_lines=shift;
  my @charsets=@_;

  my $hln=0;
  for my $hl (split (/\n/, $header_lines)) {
    $hln++;
    chomp $hl;
    # get the pair (header_name,value) into ($1,$2)
    if ($hl =~ /^([^:]+):\s+(.*)/) {
      my $h_entry=$1;
      my $h_line=$2;
      my $eh_line=$h_line;			# encoded header line
      my $eh_charset;
      if ($h_line =~ /[^\x20-\x7F]/) { # if at least one non-ascii character
	foreach (@charsets) {
	  eval {
	    $eh_line = Encode::encode($_, $h_line, Encode::FB_CROAK|Encode::LEAVE_SRC);
	  };
	  if (!$@) {	# encoding is OK
	    $eh_charset=$_;
	    last;
	  }
	}
	if (!defined $eh_charset) {
	  die "Unable to encode outgoing header entry at line $hln with any of the specified charsets (See 'preferred_charset' configuration parameter)";
	}
      }
      my $v = my_encode_mimewords($eh_line, $eh_charset);

      $top->head->replace($h_entry, $v);

    }
    else {
      # we couldn't find 'header_name: header_value'
      warn "Unrecognized header entry '$hl' at line $hln for outgoing mail\n";
    }
  }
}

sub add_date_header {
  my $top=shift;
  # rfc2822 format
  my $old_l = setlocale(LC_TIME, "C");
  my $date = strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time()));
  $top->head->replace("Date", $date);
  setlocale(LC_TIME, $old_l);
}

sub encode_text_body {
  my $db_body=shift;
  my @charsets = @_;

  my $body;
  # try the different charsets in the order of their declaration and
  # keep the one with which encode() produces no error
  my $body_charset;
  foreach (@charsets) {
    eval {
      $body = Encode::encode($_, $db_body, Encode::FB_CROAK|Encode::LEAVE_SRC);
    };
    if (!$@) {
      $body_charset=$_;
      last;
    }
  }

  if (!defined $body_charset) {
    die "Unable to encode body of outgoing mail with any of the specifieds charset (See 'preferred_charset' configuration parameter)";
  }

  my $format_flowed;
  my $sep2="\n";
#  if (getconf_bool("body_format_flowed", $mbox)) {
#    $format_flowed = "; format=flowed";
#    $sep2 = " \n";
#  }
  local $Text::Wrap::separator2=$sep2;
  local $Text::Wrap::columns=78;
  my @paragraphs = split(/\n/, $body);
  $body="";
  foreach (@paragraphs) {
    if (substr($_,0,1) ne ">") { # don't wrap quoted contents
      $body .= wrap('', '', $_) . "\n";
    }
    else {
      $body .= "$_\n";
    }
  }
  return ($body, $body_charset);
}


# Extract fields from a header date
# return undef on parse error or a list:
# ($secs, $min, $hour, $day, $month(1->12), $year(4 digits), $tz_offset(+|-\d{4})
sub parse_sender_date {
  my ($d) = @_;
  my %days = ('sun',1,'mon',2,'tue',3,'wed',4,'thu',5,'fri',6,'sat',7);
  my %months = ('jan',1,'feb',2,'mar',3,'apr',4,'may',5,'jun',6,'jul',7,
                'aug',8,'sep',9,'oct',10,'nov',11,'dec',12);
  my ($year,$month,$day);
  my $strtim;
  $d =~ s/^\s+(.*)/$1/;
  # see if it starts with the day's name
  my @words = split (/\W/, $d, 2);
  my @dat = split (/\s+/, $d);
  if (!$days{lc($words[0])}) {
    # if the day's name is missing, prepend a dummy name
    @dat = ('day,', @dat);
  }
  # if HH:MM:SS or HH:MM comes before the year
  if (defined($dat[3]) && $dat[3] =~ /\d\d\:\d\d/) {
    $year=$dat[4];
    $strtim=$dat[3];
  }
  else {
    $year=$dat[3];
    $strtim=$dat[4]
  }
  if (exists $months{lc($dat[2])}) {
    $month=$months{lc($dat[2])};
    $day=$dat[1];
  }
  elsif (exists $months{lc($dat[1])}) {
    $month=$months{lc($dat[1])};
    $day=$dat[2];
  }
  else {
    return undef;
  }
  return undef if ($day<1 || $day>31);

  my $tz="";
  if ($dat[5] =~ /^[+\-](\d\d)(\d\d)$/) {
    if ($1<=13 && $2<60) { # NZDT is UTC+1300
      $tz=" $dat[5]";
    }
  }
  return undef if ($strtim !~ /^\d{1,2}:\d{1,2}:\d{1,2}$/);
  my ($hour,$min,$secs)= split (/:/, $strtim);
  return undef if ($hour>=24 || $min>=60 || $secs>=60);

  $year += 2000 if ($year<50);
  my $t = POSIX::mktime($secs, $min, $hour, $day,
			$month-1, $year-1900, 0, 0, -1);
  if (defined $t) {
    # mktime does not fail on certain invalid dates.
    # that's why we convert mktime's result back to a date in
    # localtime and compare that to the day/month/year that
    # we passed. If they're different, that was an invalid date.
    my $ymd = strftime("%Y-%m-%d", localtime($t));
    return undef if ($ymd ne sprintf("%04d-%02d-%02d", $year, $month, $day));
  }
  return (defined $t) ? ($year,$month,$day,$hour,$min,$secs,$tz) : undef;
}

# Convert a header date to "YYYY-MM-DD HH:MM:SS [+TZ]" format
# Returns undef if it appears impossible
sub reformat_sender_date {
  my ($y,$m,$d,$h,$mn,$s,$tz) = parse_sender_date(@_);
  if (defined $y) {
    return sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
		   $y, $m, $d, $h, $mn, $s, $tz);
  }
  else {
    return undef;
  }
}

# Convert a header date to an UTC timestamp
# Returns undef on parse error
sub convert_sender_date_to_timestamp {
  my ($y,$m,$d,$h,$mn,$s,$tz) = parse_sender_date(@_);
  if (defined $y) {
    my $t = POSIX::mktime($s, $mn, $h, $d, $m-1, $y-1900);
    return undef if (!defined $t);
    if ($tz =~ /^\s+([+-])(\d{2})(\d{2})$/) {
      if ($1 eq "+") {
	$t -= $2*3600+$3*60;
      }
      elsif ($1 eq "-") {
	$t += $2*3600+$3*60;
      }
    }
    return $t;
  }
  else {
    return undef;
  }
}

1;

HTML source code generated by GNU Source-Highlight plus some custom post-processing

List of all available source files