1 # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
4 # Mail Delivery Agent module, delivers mail into a ssoma git repo
8 use Ssoma::GitIndexInfo;
11 my ($class, $git) = @_;
12 bless { git => $git, ref => "refs/heads/master" }, $class;
15 # may convert existing blob to a tree
16 # returns false if message already exists
17 # returns true on successful delivery
19 my ($self, $gii, $new, $path) = @_;
21 my $git = $self->{git};
22 my $obj = "$self->{ref}^0:$path";
23 my $cur = $git->blob_to_simple($obj);
25 # do nothing if the messages match:
26 return 0 if $git->simple_eq($cur, $new);
31 # implicitly create a new tree via index with two messages
32 foreach my $simple ($cur, $new) {
33 my $id = $git->simple_to_blob($simple);
34 my $path2 = $git->hash_simple2($simple);
35 $gii->update("100644", $id, "$path/$path2");
40 # used to update existing trees, which only happen when we have Message-ID
43 my ($self, $gii, $new, $path) = @_;
44 my $git = $self->{git};
45 my $obj = "$self->{ref}^0:$path";
46 my $cmd = "git ls-tree $obj";
48 $? == 0 or die "$cmd failed: $!\n";
51 my $id = $git->simple_to_blob($new);
52 my $path2 = $git->hash_simple2($new);
54 # go through the existing tree and look for duplicates
55 foreach my $line (@tree) {
56 $line =~ m!\A100644 blob ([a-f0-9]{40})\t(([a-f0-9]{40}))\z! or
57 die "corrupt repo: bad line from $cmd: $line\n";
58 my ($xid, $xpath2) = ($1, $2);
60 # do nothing if most of the message matches
61 return 0 if $path2 eq $xpath2 || $id eq $xid;
64 # no duplicates found, add to the index
65 $gii->update("100644", $id, "$path/$path2");
68 # this appends the given message-id to the git repo, requires locking
69 # (Ssoma::Git::sync_do)
71 my ($self, $path, $simple, $once) = @_;
73 my $git = $self->{git};
74 my $ref = $self->{ref};
76 # $path is a path name we generated, so it's sanitized
77 my $gii = Ssoma::GitIndexInfo->new;
79 my $obj = "$ref^0:$path";
80 my $cmd = "git cat-file -t $obj";
81 my $type = `$cmd 2>/dev/null`;
83 if ($? == 0) { # rare, object already exists
86 my $mid = $simple->header("Message-ID");
87 die "CONFLICT: Message-ID: $mid exists ($path)\n";
90 # we return undef here if the message already exists
91 if ($type eq "blob") {
92 # this may upgrade the existing blob to a tree
93 $self->blob_upgrade($gii, $simple, $path) or return;
94 } elsif ($type eq "tree") {
95 # possibly add object to an existing tree
96 $self->tree_update($gii, $simple, $path) or return;
98 # we're screwed if a commit/tag has the same SHA-1
99 die "CONFLICT: `$cmd' returned: $type\n";
101 } else { # new message, just create a blob, common
102 my $id = $git->simple_to_blob($simple);
103 $gii->update('100644', $id, $path);
105 my $subject = $simple->header("Subject");
106 $git->commit_index($gii, 0, $ref, $subject);
109 # the main entry point takes an Email::Simple object
111 my ($self, $simple, $once) = @_;
112 my $git = $self->{git};
114 # convert the Message-ID into a path
115 my $mid = $simple->header("Message-ID");
117 # if there's no Message-ID, generate one to avoid too many conflicts
119 if (!defined $mid || $mid =~ /\A\s*\z/) {
120 $mid = '<' . $git->hash_simple2($simple) . '@localhost>';
121 $simple->header_set("Message-ID", $mid);
123 my $path = $git->mid2path($mid);
125 # kill potentially confusing/misleading headers
126 foreach my $d (qw(lines content-length)) {
127 $simple->header_set($d);
131 $git->tmp_index_do(sub {
132 $self->append($path, $simple, $once);
135 $git->sync_do(sub { $git->tmp_git_do($sub) });