Home > Archives > 2009-07

2009-07

Email::MIMEの生成をtemplateエンジン経由で行ってくれるEmail::MIME::Kitがいい感じ

  • 2009-07-12 (日)
  • perl
  • hatena count

Email::MIME::Kitがいい感じです。内部ではEmail::MIME::Creatorを使っているのですが、MIME::LiteでいうところのMIME::Lite::TTのようなもので、更にheaderに関してもtemplateで書けます。

素のEmail::MIME::Creatorで

use strict;
use warnings;
use utf8;

use Encode;
use Email::MIME;
use Email::MIME::Creator;
use Email::Send;
use Template;

my $t = Template->new({ENCODING => 'UTF-8'});
my $mydata = {hoge => 'aaa'};

$t->process('subject.tt', $mydata, \my $subject);
$t->process('body.tt', $mydata, \my $body);

my $mail = Email::MIME->create(
    header => [
        From    => 'chiba+from@geminium.com',
        To      => 'chiba+to@geminium.com',
        Subject => Encode::encode('MIME-Header', $subject),
    ],
    attributes => {
        content_type => 'text/plain',
        charset      => 'UTF-8',
    },
    body => $body,
);

my $sender = Email::Send->new({mailer => 'SMTP', mailer_args => [Host => 'localhost']});
$sender->send($mail);

って感じで書いてたものをEmail::MIME::Kitを使うと

use strict;
use warnings;
use utf8;

use Email::Send;
use Email::MIME::Kit;

my $sender = Email::Send->new({mailer => 'SMTP', mailer_args => [Host => 'localhost'] });

my $kit = Email::MIME::Kit->new({
    manifest_reader => 'YAML',
    source => 'mkit/',
});

$sender->send(
    $kit->assemble({
        hoge => 'fuga',
    })
);

という感じでシンプルに書けます。

mkit/manifest.yamlにはこんな感じで設定を書いておくと、body.ttが自動で読み込まれますし、headerの部分は外部ファイルにはできませんがTT書式で書けます。ここではrendererとしてTTを指定していますが、MicroMasonText::TemplateあたりはCPANにアップされています。

renderer: TT
header:
  - Subject: 'てすと[% hoge %]'
  - From: 'Masahiro Chiba <chiba+from@geminium.com>'
  - To: 'Masahiro Chiba <chiba+to@geminium.com>'
attributes:
  content_type: text/plain
  charset: UTF-8
path: body.tt

マルチバイト文字コードはUTF-8のことしかほぼ考えられてないみたいなのでISO-2022-JPじゃないとだめだーって人は自分でEmail::MIME::Kit::Role::Assemblerの継承クラスを定義するといいと思います。Mooooseなモジュールなのでモダンに書けます。

で、標準のTTのrenderでちょっと不満だったのがprocessに渡されるのがテンプレートファイルの中身のリファレンスで、これだとTTがコンパイル結果をキャッシュしてくれないんですよね。ファイル名で渡しておくとキャッシュしてくれる。なのでpathが指定されている場合はrendererにpathをそのままスルーしてくれるAssemblerを書いてみました。

package Email::MIME::Kit::Assembler::PathThrough;
use Moose;
extends 'Email::MIME::Kit::Assembler::Standard';
with 'Email::MIME::Kit::Role::Assembler';
our $VERSION = '1.00';

use Template;

override '_assemble_from_kit' => sub {
    my ($self, $stash) = @_;

    my $fullpath = File::Spec->catfile($self->kit->source, $self->manifest->{path});
    my $body_ref = $self->render($fullpath, $stash);

    my %attr = %{ $self->manifest->{attributes} || {} };
    $attr{content_type} = $attr{content_type} || 'text/plain';

    if ($$body_ref =~ /[\x80-\xff]/) {
        $attr{encoding} ||= 'quoted-printable';
        $attr{charset}  ||= 'utf-8';
    }

    my $email = $self->_contain_attachments({
        attributes => \%attr,
        header     => $self->manifest->{header},
        stash      => $stash,
        body       => $$body_ref,
        container_type => $self->manifest->{container_type},
    });
};

after '_set_renderer' => sub {
    my $self = shift;
    $self->renderer->_tt(
        Template->new({
            ABSOLUTE => 1,
            ENCODING => 'UTF-8',
        }),
    );
};

no Moose;
1;

まぁやっぱりUTF-8前提だったりします。

manifestにはこんな感じで「assembler: PathThrough」を追加しておきます。

assembler: PathThrough
renderer: TT
header:
  - Subject: 'てすと[% hoge %]'
  - From: 'Masahiro Chiba <chiba+from@geminium.com>'
  - To: 'Masahiro Chiba <chiba+to@geminium.com>'
attributes:
  content_type: text/plain
  charset: UTF-8
path: body.tt

HTML::FillInForm::Liteの使いどころ

  • 2009-07-12 (日)
  • perl
  • hatena count

正規表現でHTML::FillInFormを実現しているHTML::FillInForm::Liteを最近つかっています。HTML::FillInFormの機能に加え、任意のオブジェクトを渡してそのアクセサからデータを取得してくれるため、DBIx::Class::AsFdatなどのモジュールを使わずに、DBICのrowデータをそのままfillinできるのがうれしかったりします。また、パラメータの取得方法部分もHTML::FillInFormよりも比較的拡張しやすくなっているので便利です。

ただ、性能がHTML::FillInFormよりもいいかというと自分の環境下やデータではそうでもなかったりして、特に大きいhtmlデータが対象だとかなり性能が劣化するようです。formのidをtargetで指定してあげることで大分緩和されますがHTML::FillInFromとの差は歴然としてあるようです。

そこで、C::P::FillInFormのようにhtml全体を対象とするのではなく、TTのフィルターを使ってfillinしてみました。

こんな感じでFILTERを定義して、

my $t = Template->new({
    ENCODING => 'UTF-8',
    FILTERS => {
        fillinform => [\&fillinform, 1],
    },
});
my $f = HTML::FillInForm::Lite->new;
sub fillinform {
    my ($context, $data, @options) = @_;

    return sub {
        my $html = shift;

        $f->fill(\$html, $data, @options);
    };
}

テンプレートの中ではこんな風に使います。

[% FILTER fillinform(filldata) %]<form>...</form>[% END %]

formの部分は別ファイルにしておくとこんなふうにシンプルにかけるのがいい感じです。

[% INCLUDE parts/form.tt | fillinform(filldata) %]

fillinformってのはCではなくてVの仕事なんじゃないかとも思うのでこちらのほうがわかりやすいような気もしてきてます。

で、最後にベンチマークをのっけておきます。全体のfillinと局所化したものがだいぶ差があるのがわかると思います。また、それでもHTML::FillInFormに勝てていませんが、大分差も縮まっています。

use strict;
use warnings;
use utf8;
binmode STDOUT, ':utf8';

use Benchmark qw/cmpthese/;
use Template;
use HTML::FillInForm;
use HTML::FillInForm::Lite;

my $t = Template->new({
    ENCODING => 'UTF-8',
    FILTERS => {
        fillinform => [\&fillinform, 1],
        fillinformlite => [\&fillinformlite, 1],
    },
});
my $f = HTML::FillInForm->new;
my $fl = HTML::FillInForm::Lite->new;
my $filldata = {mail => 'chiba@geminium.com', name => '千葉征弘', tel => '03-3419-2801'};

cmpthese(5000, {
    fillinall => sub {
        $t->process('test.html', {
            hoge => 'aha'
        }, \my $output);

        $f->fill(\$output, $filldata);
    },
    fillinpart => sub {
        $t->process('test_part.html', {
            hoge => 'aha',
            filldata => $filldata,
        }, \my $output);
    },
    fillinall_lite => sub {
        $t->process('test.html', {
            hoge => 'aha'
        }, \my $output);

        $fl->fill(\$output, $filldata);
    },
    fillinpart_lite => sub {
        $t->process('test_part_lite.html', {
            hoge => 'aha',
            filldata => $filldata,
        }, \my $output);
    },
});
sub fillinform {
    my ($context, $data, @options) = @_;

    return sub {
        my $html = shift;

        $f->fill(\$html, $data, @options);
    };
}
sub fillinformlite {
    my ($context, $data, @options) = @_;

    return sub {
        my $html = shift;

        $fl->fill(\$html, $data, @options);
    };
}

test.html
test_part.html
test_part_lite.html

結果

                  Rate fillinall_lite     fillinall fillinpart_lite   fillinpart
fillinall_lite   274/s             --          -62%            -76%         -77%
fillinall        729/s           166%            --            -36%         -40%
fillinpart_lite 1136/s           315%           56%              --          -6%
fillinpart      1208/s           341%           66%              6%           --

Catalyst::Develを1.16以上にUpgradeしたとき

  • 2009-07-12 (日)
  • perl
  • hatena count

catalystのstandaloneサーバを-r付きで使っている人はCatalyst::Develを1.16以上にUpgradeしたときは

catalyst.pl -force -scripts MyApp

を自分のmy appディレクトリで実行すること。しないと_kill_childメソッドが無いとかエラーがでます。

Home > Archives > 2009-07

Search
Feeds
Meta

Return to page top