Mailmerge with Perl

  • ... and Word/Excel and PDF24 on Windows
  • Custom mail ...
  • with optional custom PDF-attachment
  • All in one directory (USB-drive)
  • ... also Strawberry Perl portable

1. step: Mail template

  • I get it via mail or send it to myself.
  • Get it as MIME from server
  • and save it as .eml-file
  • You can open it with Thunderbird or theBat (not Outlook)
  • ... or any text-editor (e.g. remove "Received:")

Directory

  • perl - subdir with strawberry portable
  • setcmd.cmd - put local Perl in PATH

set PATH=perl\perl\bin;%PATH%
cmd

				
  • Newest mail as template from server - getmail.pl
  • 
    my $pop = new Mail::POP3Client(
    	USER     => "<username>",
    	PASSWORD => "<password>",
    	HOST     => "pop3.mailserver.de",
    	USESSL   => 1,
    );						   
    open my $fh, ">:raw", "mail.eml" or die "open out";
    $pop->HeadAndBodyToFile( $fh, $pop->Count() );
    $pop->Close();
    • View in Thunderbird (with doubleclick)
    • or move to draft for editing
    Mail view in Thunderbird

    2. step: Data as csv-file

    • that is what I get: notpaid.csv

    Testdata
    • and some extra infos
    • in adressen.csv

    extra testdata

    convert for serial letter

    • one line for each invoice (mail): file.csv

    file.csv
    • and one line for each invoice item
    • 20 lines for each invoid/mail: codelist.csv
      empty lines if less that 20 items

    codelist.csv
  • That's easy in Perl. Is it?
    Read address infos into a hash:
  • 
    my %adr;
    open ADR, "<", "adressen.csv" or die "open ...";
    <ADR>;	# skip header
    while (<ADR>) { chomp;
    	my @a = split ";", $_;
    	my $email = shift @a;
    	$adr{$email} = join ";", @a;
    }
    close ADR;
    
  • Open and close some files
  • 
    open IN, "<", $filename or die "open in $filename";
    ...
    open FILE, ">", "file.csv" or die "open out file";
    print FILE "Nr;Summe;EMail;Name;Ignore;EMail;pdf\n";
    open LIST, ">", "codelist.csv" or die "open out...";
    print LIST "nr;email;name;...;ts;price\n";
    ...
    close <everything>;
    
  • Loop over data lines (items)
  • 
    <IN>;	# skip first line (header)
    while (<IN>) {
    	chomp;
    	my @a = split ";", $_;
    	my $mail = $a[4];
    	
    	next if $a[1] ne "false";
    	...
    }
    • pf() for each new mail
    • and count codes per page
    
    	pf() if defined($last_m) and $mail ne $last_m;
    	$codepp++;
    	my $price = $a[8];
    	$sum += $price;
    	print LIST "$codepp;$mail;$a[5];..;$price\n";
    
    	$last_m = $mail;
    }
    pf();
    • pf() for each new mail
    • and count codes per page
    • fill with empty lines for 20 per page
    
    sub pf { # print into file.csv
    	$c_mail++;
    	my $nr = sprintf("%03d", $c_mail);
    	my $adr = $adr{$last_mail};
    	print FILE "$c_mail;$sum;$adr;;$last_mail;pdf.\n";
    
    	print LIST ";;;;;;;;;\n" while $codepp++ < 20;
    	$codepp = 0; $sum = 0;
    }
    
    • Serial letter in Word for all attachments
    • Data source is codelist.csv

    mergetop
    • and one line for each invoice item
    • 20 lines for each invoid/mail: codelist.csv
      empty lines if less that 20 items

    codelist.csv
    • The sum can be done in Word table as =SUM(ABOVE)

    mergebottom
    • Merging all data gives a doc with all invoices

    allattachments
    • One invoice per page

    oneattachment
    • Split pages into files

    pdf24split
    • With GUI or command line:
      "C:\Programs\PDF24\pdf24-DocTool.exe" -splitByPage
      -outputDir . -outputFile pdf Codes.pdf

    pdffiles

    remember this slide?

    • one line for each invoice (mail): file.csv
    • actually now: file.xlsx

    file.csv

    and now ... finally sending: send.pl

  • first parsing Excel file
  • 
    my $parser = Spreadsheet::ParseXLSX->new;
    my $workbook = $parser->parse("file.xlsx");
    my $worksheet = $workbook->worksheet(0);
    my ( $row_min, $row_max ) = $worksheet->row_range();
    my ( $col_min, $col_max ) = $worksheet->col_range();
    for my $r ($row_min .. $row_max) {
      for my $c ( $col_min .. $col_max ) {
    	my $cell = $worksheet->get_cell($r, $c);
    

    Field "To:" and mail template

    
    	my $to = $feldval[$col_email];
    	utf8::decode($to);
    	$to =~ s/[;\n]/,/gm;	# there might be more 
    	...
    	open IN, "<", $filename or die "open $filename";
    	my $mime = Email::MIME->new(join('', <IN>));
    	close IN;
    	$mime->header_str_set("To" => $to);
    

    Setting "Date:" and "Message-Id:"

    
    	my $date = email_date; 		# now
    	print "date: $date\n";
    	$mime->header_str_set("Date" => $date);
    
    	my $msgid = Email::MessageID->new(host=>'domain.de')
    							->in_brackets;
    	$mime->header_str_set("Message-Id" => $msgid);
    

    We need to walk the template

    
    	$mime->walk_parts(sub { my ($part) = @_;	  
    	  if ( $part->content_type =~ m[text/plain]i
    			or $part->content_type =~ m[text/html]i ) {
    		my $b = $part->body;
    		$b =~ s/\r\n\r\n/\n/g;	# convert newlines
    		...
    		$part->body_set($b);
    	  }
    	});
    

    Template variable substitution

    
    		for my $c ($col_min .. $col_email) {
    		  my $s=$feldval[$c];
    		  if ($part->content_type 
    				=~ m[charset="iso-8859-1"]i) {
    		    $s = Encode::encode("ISO-8859-1", $s);
    		  }
    		  $b =~ s/%%$feld[$c]%%/$s/gm;
    		}
    

    If template without attachments

    
    		if ( $mime->content_type
    					=~ m[multipart/alternative]i ) {
    			$mime->content_type_set( 'multipart/mixed' );
    			my @parts = $mime->subparts;
    			my $neu = Email::MIME->create(parts => [@parts] );
    			$neu->content_type_set('multipart/alternative');
    			$mime->parts_set( [ $neu ] );
    		}
    

    Attach the pdf-file

    
    		my @parts = (	Email::MIME->create(
    				attributes => {
    					filename     => "$name$suffix",
    					content_type => "application/pdf",
    					disposition  => "attachment",
    					encoding     => "base64",
    					name         => "$name$suffix",
    				}, body => io($feldval[$c])->binary->all));
    		$mime->parts_add( [@parts] );
    

    Sending

    
    	my $mime_as_string=$mime->as_string;
    	$mime_as_string =~ s/\r\n/\n/gm;
    	utf8::downgrade($mime_as_string);
    	if (!$dryrun) {	# really send
    	  my $sender = Email::Send->new({mailer => 'SMTP'});
    	  $sender->mailer_args([Host => 'smtp.host.de:465',
    	  ssl => 1, username => 'u', password => 'p']);
    	  $sender->send($mime_as_string); sleep 10;
    	}
    
    • There was even some more fiddling with header and encoding
    • and logging of cource.
    • Bounces and errors still need manual care

    Thank you

    • Slides: https://www.wieland-pusch.de/slides/gpw2022/
    • Code: mail me wieland@wielandpusch.de