2 # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
4 # This is the normal command-line client for users
10 use File
::Path
::Expand qw
/expand_filename/;
11 use File
::Path qw
/make_path/;
12 use File
::Temp qw
/tempfile/;
14 use Email
::LocalDelivery
;
15 Getopt
::Long
::Configure
("require_order", "pass_through");
18 "help|h" => \
$opts{help
},
19 "quiet|q" => \
$opts{quiet
},
20 "force|f" => \
$opts{force
},
23 $ENV{SSOMA_HOME
} ||= expand_filename
("~/.ssoma/");
25 # these expand automatically to the associated cmd_$name, so "add"
26 # calls cmd_add, "sync" calls cmd_sync, and so forth
29 doc
=> "start watching a new list",
30 arg
=> "LISTNAME URL TYPE:/path/to/destination [TARGET]",
31 long
=> "TYPE must be one of 'maildir', 'mbox', 'imap' ".
35 doc
=> "sync target(s) for existing LISTNAME",
36 arg
=> "[LISTNAME] [TARGET]",
39 doc
=> "show a message by Message-ID",
40 arg
=> "MESSAGE-ID [LISTNAME|GIT_DIR]",
44 my $cmd = shift @ARGV;
45 usage
("", 1) unless defined $cmd;
46 $cmd eq "help" and usage
("", 0);
47 $cmd{$cmd} or usage
("", 1);
52 } or die "BUG: $cmd not implemented\n";
58 my ($cmd, $exit) = @_;
59 my $fd = $exit ? \
*STDERR
: \
*STDOUT
;
60 print $fd "Usage: ssoma <command> [options] [arguments]\n";
62 print $fd "Available commands:\n" unless $cmd;
64 foreach my $c (sort keys %cmd) {
65 next if $cmd && $cmd ne $c;
67 print $fd ' ', pack($pad, $c), $cmd{$c}->{doc
}, "\n";
68 print $fd ' ', pack($pad, ''), $cmd{$c}->{arg
}, "\n";
70 my $long = $cmd{$c}->{long
};
72 print $fd ' ', pack($pad, ''), $long, "\n";
75 my $opt = $cmd{$c}->{opt
} or next;
76 foreach my $s (sort keys %$opt) {
77 # prints out arguments as they should be passed:
78 my $x = s
#[:=]s$## ? '<arg>' :
79 (s
#[:=]i$## ? '<num>' : '');
80 print $fd ' ' x
21, join(', ', map { length $s > 1 ?
82 split /\|/, $s)," $x\n";
91 $name =~ /\A[a-zA-Z0-9]/ or die
92 "LISTNAME must start with an alphanumeric char\n";
93 $name =~ /[a-zA-Z0-9]\z/ or die
94 "LISTNAME must end with an alphanumeric char\n";
95 $name =~ /\A[\w\.\-]+\z/ or die
96 "LISTNAME must only contain alphanumerics, dashes, periods and underscores\n";
100 my ($listname, $url, $dest, $target) = @_;
101 (defined($url) && defined($listname) && defined($dest)) or
104 check_listname
($listname);
106 $dest =~ /\A(mbox|maildir|command|imaps?):(.+)\z/ or
109 my ($type, $path) = ($1, $2);
112 if ($type =~ /\Aimaps?\z/) {
115 $path = File
::Spec
->rel2abs($path);
118 # Email::LocalDelivery relies on this trailing slash for
119 # maildir distinction
120 if (($type eq "maildir") && ($path !~ m!/\z!)) {
122 } elsif (($type eq "mbox") && ($path =~ m!/\z!)) {
123 die "mbox `$path' must not end with a trailing slash\n";
126 $target = "local" unless defined $target;
128 my $dir = "$ENV{SSOMA_HOME}/$listname.git";
129 make_path
($ENV{SSOMA_HOME
});
130 my $git = Ssoma
::Git
->new($dir);
132 push @init_args, '-q' if $opts{quiet
};
133 $git->init_db(@init_args);
134 my $state = "$git->{git_dir}/ssoma.state";
137 local $ENV{GIT_CONFIG
} = "$git->{git_dir}/config";
140 # no imap:// support in URI, yet, but URI has ftp://
143 $uri =~ s{\A(imaps?):}{ftp:};
145 my $u = URI
->new($uri);
147 $u->scheme or die "no scheme from $dest\n";
148 defined(my $host = $u->host) or die "no host from $dest\n";
149 my $port = $u->_port;
150 x
(qw
/git config imap.port/, $port) if (defined $port);
151 x
(qw
/git config imap.host/, "$scheme://$host");
153 defined(my $user = $u->user) or die "no user in $dest\n";;
154 x
(qw
/git config imap.user/, $user);
155 my $p = $u->password;
156 warn_imap_pass
($ENV{GIT_CONFIG
}) if (defined $p);
159 defined $path or $path = "INBOX";
160 $path =~ s!\A/!!; # no leading slash
161 x
(qw
/git config imap.folder/, $path);
163 # this only needs to be set for Extractor to follow
164 local $ENV{GIT_CONFIG
} = $state;
165 x
(qw
/git config/, "target.$target.imap", "true");
167 local $ENV{GIT_CONFIG
} = $state;
168 my $cfg = $type eq "command" ?
"command" : "path";
169 x
(qw
/git config/, "target.$target.$cfg", $path);
172 $git->tmp_git_do(sub {
173 x
(qw
/git remote add --mirror=fetch origin/, $url);
179 foreach my $dir (glob("$ENV{SSOMA_HOME}/*.git")) {
186 my ($listname, @targets) = @_;
187 if (defined $listname) {
188 check_listname
($listname);
189 do_sync
("$ENV{SSOMA_HOME}/$listname.git", \
@targets);
191 foreach_list
(sub { do_sync
($_[0], []) });
196 my ($message_id, $listname) = @_;
198 # write to a temporary mbox because Email::LocalDelivery works
200 my ($fh, $mbox) = tempfile
(TMPDIR
=> 1, SUFFIX
=> '.mbox');
202 if (defined $listname) {
203 my $path = -d
$listname ?
$listname
204 : "$ENV{SSOMA_HOME}/$listname.git";
205 do_cat
($path, $message_id, $mbox);
207 foreach_list
(sub { do_cat
($_[0], $message_id, $mbox, 1) });
209 unlink $mbox or warn "error unlinking $mbox: $!\n";
212 print $_ or warn "failed printing to stdout: $!\n";
214 close $fh or die "error closing $mbox: $!\n";
218 my ($dir, $targets) = @_;
219 my $git = Ssoma
::Git
->new($dir);
220 my $ex = Ssoma
::Extractor
->new($git);
222 # no targets? sync all of them
223 if (scalar(@
$targets) == 0) {
224 my $cfg = $git->config_list("$git->{git_dir}/ssoma.state");
226 foreach my $k (keys %$cfg) {
227 $k =~ /\Atarget\.(\w+)\.(?:path|imap|command)\z/
234 $git->tmp_git_do(sub {
235 my @cmd = qw
/git fetch/;
236 push @cmd, '-q' if $opts{quiet
};
237 push @cmd, '-f' if $opts{force
};
241 foreach my $target (@
$targets) {
242 $ex->extract($target);
247 system(@_) and die join(' ', @_). " failed: $?\n";
253 ignoring IMAP password given on command-line
254 ensure $file is not world-readable before editing
255 $file to set imap.pass
260 my ($dir, $message_id, $mbox, $missing_ok) = @_;
261 my $git = Ssoma
::Git
->new($dir);
262 my $ex = Ssoma
::Extractor
->new($git);
263 $ex->midextract($message_id, $mbox, $missing_ok);