Applying patch to Publish::Maildir

先日から再考および試験を行っていた、Publish::Maildirへのパッチが落ち着きをみせたため、公開します。

plagger/lib/Plagger/Plugin/Publish/Maildir.pm:

package Plagger::Plugin::Publish::Maildir;
use strict;
use base qw( Plagger::Plugin );

use DateTime;
use DateTime::Format::Mail;
use Encode qw/ from_to encode/;
use Encode::MIME::Header;
use MIME::Lite;
use Digest::MD5 qw/ md5_hex /;;
use File::Find;
use IO::File;

sub register {
    my($self, $context) = @_;
    $self->{version} = '0.1';
    $context->register_hook(
      $self,
      'publish.init' => ?&initialize,
      'publish.entry.fixup' => ?&store_entry,
       'publish.finalize' => ?&finalize,
    );
}

sub initialize {
  my ($self, $context, $args) = @_;
  my $cfg = $self->conf;
  my $permission = $cfg->{permission} || 0700;
  if (-d $cfg->{maildir}) {
    my $path = "$cfg->{maildir}/.$cfg->{folder}";
       $path =~ s/?/?//?//g;
       $path =~ s/?/$//g;
    unless (-d $path) {
      mkdir($path,0700)
        or die $context->log(error => "Could not create $path");
      $context->log(info => "Create new folder ($path)");
    }
    unless (-d $path."/new") {
      mkdir($path."/new",0700)
        or die $context->log(error => "Could not Create $path/new");
      $context->log(info => "Create new folder($path/new)");
    }
    $self->{path} = $path;

    $self->precache($context);
  }else{
    die $context->log(error => "Could not access $cfg->{maildir}");
  }
}

sub precache {
  my ($self, $context) = @_;


  my $conf = $self->conf;


  my $paths = {
    cur => $self->{path} . '/cur',
  };

  foreach my $path (keys %$paths) {
    next unless (-d $paths->{$path});
    
    $self->{cache}->{$path}->{dirname} = $paths->{$path};
    
    $self->{cache}->{$path}->{entries}->{''} = undef;
    
    $context->log(debug => "Searching entries($path)...");
    
    find(
      sub {
        my ($d) = $_ =~ /^([0-9a-z]*)?.plagger/;
        
        return unless defined $d;
        
        $self->{cache}->{$path}->{entries}->{$d} = $_;
      },
      $self->{cache}->{$path}->{dirname});
  }

  
  return unless ($conf->{keep_deleted});


  $paths = {deleted => $conf->{maildir} . '/' .
                       ($conf->{trash} || '.Deleted Messages'),
  };

  foreach my $path (keys %$paths) {
    next unless (-d $paths->{$path});
 
    $self->{cache}->{$path}->{dirname} = $paths->{$path};
    
    $self->{cache}->{$path}->{entries}->{''} = undef;
    
    $context->log(debug => "Searching entries($path)...");
    
    find(
      sub {
        return unless (-f $_);
        return unless (/^?d{10}?./);
        
        if (my $fh = IO::File->new($_)) {
          while (my $line = <$fh>) {
            last if ($line =~ /^?n+/);
            
            my ($digest) = $line =~ /^Message-Id:?s+<([0-9a-f]+)/i;
            
            if (defined $digest) {
              $self->{cache}->{$path}->{entries}->{$digest} = $_;

              last;
            }
          }
          $fh->close;
        }
      },
      $paths->{$path});
  }
}

sub finalize {
  my ($self, $context, $args) = @_;
  if (my $msg_count = $self->{msg}) {
    if (my $update_count = $self->{update_msg}) {
      $context->log(info => "Store $msg_count message(s) ($update_count message(s) updated)");
    }else{
      $context->log(info => "Store $msg_count message(s)");
    }
  }
}

sub store_entry {
  my($self, $context, $args) = @_;
  my $cfg = $self->conf;
  my $msg;
  my $entry = $args->{entry}; 
  my $feed_title = $args->{feed}->title;
     $feed_title =~ tr/,//d;
  my $subject    = $entry->title || '(no-title)';
  my $body       = $self->templatize($context, $args);
  my $from       = $cfg->{mailfrom} || 'plagger@localhost';
  my $now = Plagger::Date->now(timezone => $context->conf->{timezone});

  my $digest = md5_hex($entry->id_safe);
  my $id     = $digest . '.plagger@localhost';

  $msg = MIME::Lite->new(
    Date    => $now->format('Mail'),
    From    => encode('MIME-Header', qq("$feed_title" <$from>)),
    To      => $cfg->{mailto},
    Subject => encode('MIME-Header', $subject),
    Type    => 'multipart/related',
  );
  $body = encode("utf-8", $body);
  $msg->attach(
    Type => 'text/html; charset=utf-8',
    Data => $body,
    Encoding => 'quoted-printable',
  );
  $msg->add('Message-Id', "<$id>");
  $msg->add('X-Tags', encode('MIME-Header',join(' ',@{$entry->tags})));
  my $xmailer = "MIME::Lite (Publish::Maildir Ver.$self->{version} in plagger)";
  $msg->replace('X-Mailer',$xmailer);

  store_maildir($self, $context, $msg->as_string(), $digest);

  $self->{msg} += 1;
}

sub templatize {
  my ($self, $context, $args) = @_;
  my $tt = $context->template();
#  $tt->process( 'gmail_notify.tt', {
  $tt->process( 'mail.tt', {
    entry => $args->{entry},
    feed  => $args->{feed},
  }, ?my $out ) or $context->error($tt->error);
  $out;
}

sub store_maildir {
  my($self, $context, $msg, $digest) = @_;

  my $filename = "$digest.plagger";

  if (exists $self->{cache}->{deleted}->{entries}->{$digest}) {
    $context->log(debug => "skip: deleted: $digest");
    return;
  }
  
  if (exists $self->{cache}->{cur}->{entries}->{$digest}) {
    $context->log(debug => "found: cur: $digest");
    
    return if ($self->conf->{keep_read});

    unlink ($self->{cache}->{cur}->{dirname} . '/' .
            $self->{cache}->{cur}->{entries}->{$digest});

    $self->{update_msg} ++;
  }

  $context->log(debug => "writing: new/$filename");
    
  if (my $fh = IO::File->new('>' . $self->{path} . '/new/' . $filename)) {
    print $fh $msg;

    $fh->close;
  }
}

1;

=head1 NAME

Plagger::Plugin::Publish::Maildir - Store Maildir

=head1 SYNOPSIS

  - module: Publish::Maildir 
    config:
      maildir: /home/foo/Maildir
      folder: plagger

=head1 DESCRIPTION

This plugin changes an entry into e-mail, and saves it to Maildir.

=head1 AUTHOR

Nobuhito Sato

=head1 SEE ALSO

L<Plagger>

=cut

diff -uを用いた差分は、以下になります。

--- Maildir.pm.orig	2006-06-02 00:39:59.000000000 -0700
+++ Maildir.pm	2006-06-02 00:42:15.000000000 -0700
@@ -9,6 +9,7 @@
 use MIME::Lite;
 use Digest::MD5 qw/ md5_hex /;;
 use File::Find;
+use IO::File;
 
 sub register {
     my($self, $context) = @_;
@@ -40,11 +41,85 @@
       $context->log(info => "Create new folder($path/new)");
     }
     $self->{path} = $path;
+
+    $self->precache($context);
   }else{
     die $context->log(error => "Could not access $cfg->{maildir}");
   }
 }
 
+sub precache {
+  my ($self, $context) = @_;
+
+
+  my $conf = $self->conf;
+
+
+  my $paths = {
+    cur => $self->{path} . '/cur',
+  };
+
+  foreach my $path (keys %$paths) {
+    next unless (-d $paths->{$path});
+    
+    $self->{cache}->{$path}->{dirname} = $paths->{$path};
+    
+    $self->{cache}->{$path}->{entries}->{''} = undef;
+    
+    $context->log(debug => "Searching entries($path)...");
+    
+    find(
+      sub {
+        my ($d) = $_ =~ /^([0-9a-z]*)?.plagger/;
+        
+        return unless defined $d;
+        
+        $self->{cache}->{$path}->{entries}->{$d} = $_;
+      },
+      $self->{cache}->{$path}->{dirname});
+  }
+
+  
+  return unless ($conf->{keep_deleted});
+
+
+  $paths = {deleted => $conf->{maildir} . '/' .
+                       ($conf->{trash} || '.Deleted Messages'),
+  };
+
+  foreach my $path (keys %$paths) {
+    next unless (-d $paths->{$path});
+ 
+    $self->{cache}->{$path}->{dirname} = $paths->{$path};
+    
+    $self->{cache}->{$path}->{entries}->{''} = undef;
+    
+    $context->log(debug => "Searching entries($path)...");
+    
+    find(
+      sub {
+        return unless (-f $_);
+        return unless (/^?d{10}?./);
+        
+        if (my $fh = IO::File->new($_)) {
+          while (my $line = <$fh>) {
+            last if ($line =~ /^?n+/);
+            
+            my ($digest) = $line =~ /^Message-Id:?s+<([0-9a-f]+)/i;
+            
+            if (defined $digest) {
+              $self->{cache}->{$path}->{entries}->{$digest} = $_;
+
+              last;
+            }
+          }
+          $fh->close;
+        }
+      },
+      $paths->{$path});
+  }
+}
+
 sub finalize {
   my ($self, $context, $args) = @_;
   if (my $msg_count = $self->{msg}) {
@@ -65,8 +140,12 @@
      $feed_title =~ tr/,//d;
   my $subject    = $entry->title || '(no-title)';
   my $body       = $self->templatize($context, $args);
-  my $from       = 'plagger@localhost';
+  my $from       = $cfg->{mailfrom} || 'plagger@localhost';
   my $now = Plagger::Date->now(timezone => $context->conf->{timezone});
+
+  my $digest = md5_hex($entry->id_safe);
+  my $id     = $digest . '.plagger@localhost';
+
   $msg = MIME::Lite->new(
     Date    => $now->format('Mail'),
     From    => encode('MIME-Header', qq("$feed_title" <$from>)),
@@ -80,11 +159,13 @@
     Data => $body,
     Encoding => 'quoted-printable',
   );
+  $msg->add('Message-Id', "<$id>");
   $msg->add('X-Tags', encode('MIME-Header',join(' ',@{$entry->tags})));
   my $xmailer = "MIME::Lite (Publish::Maildir Ver.$self->{version} in plagger)";
   $msg->replace('X-Mailer',$xmailer);
-  my $filename = md5_hex($entry->id_safe);
-  store_maildir($self, $context,$msg->as_string(),$filename);
+
+  store_maildir($self, $context, $msg->as_string(), $digest);
+
   $self->{msg} += 1;
 }
 
@@ -100,21 +181,33 @@
 }
 
 sub store_maildir {
-  my($self,$context,$msg,$file) = @_;
-  my $filename = $file.".plagger";
-  find(
-    sub {
-      if ($_ =~ m!$file.*!) {
-        unlink $_;
-        $self->{update_msg} += 1;
-      }
-    },
-    $self->{path}."/cur"
-  );
-  my $filename = $self->{path}."/new/".$filename;
-  open(FILE,">$filename");
-  print(FILE $msg);
-  close(FILE);
+  my($self, $context, $msg, $digest) = @_;
+
+  my $filename = "$digest.plagger";
+
+  if (exists $self->{cache}->{deleted}->{entries}->{$digest}) {
+    $context->log(debug => "skip: deleted: $digest");
+    return;
+  }
+  
+  if (exists $self->{cache}->{cur}->{entries}->{$digest}) {
+    $context->log(debug => "found: cur: $digest");
+    
+    return if ($self->conf->{keep_read});
+
+    unlink ($self->{cache}->{cur}->{dirname} . '/' .
+            $self->{cache}->{cur}->{entries}->{$digest});
+
+    $self->{update_msg} ++;
+  }
+
+  $context->log(debug => "writing: new/$filename");
+    
+  if (my $fh = IO::File->new('>' . $self->{path} . '/new/' . $filename)) {
+    print $fh $msg;
+
+    $fh->close;
+  }
 }
 
 1;

PlaggerCommitterGuidelineMake Your Plugin Simpleの章に反するようで後ろめたい気もしますが、config:の項目を4つ増やしています*1

mailfrom MIME::Liteに渡される、From:タグの内容を指定する(デフォルトは、plagger@localhost)
keep_read 既読のエントリを再配信/未読化しない(デフォルトは、オフ(再配信/未読化をする))
keep_deleted 削除されているエントリを再配信/未読化しない(デフォルトは、オフ(再配信/未読化をする))
trash Maildir内のゴミ箱ディレクトリを指定する。maildirからの相対パスで記述する(デフォルトは、.Deleted Messages)


設定は目的によってもまちまちなのですが、Publish::Maildirの部分は、以下のように設定しています。

config.yaml:

                   :
  - module: Publish::Maildir
    config:
      maildir: /Users/username/Maildir
      folder: plagger
      mailto: me@example.com
      mailfrom: me@example.com
      keep_read: 1
      keep_deleted: 1

次回からは、このパッチを作成する際に試行した考えや、テストコーディングについて記載していきたいと思っています。

いつもながら、自分のコーディングの稚拙さと、筆の遅さには歯痒さを感じます。

*1:6/3/2006修正