summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Changes2
-rw-r--r--dist.ini5
-rw-r--r--lib/Config/ClawsMail.pm78
-rw-r--r--lib/Config/ClawsMail/Account.pm86
-rw-r--r--lib/Config/ClawsMail/MainConfigParser.pm15
-rw-r--r--lib/Config/ClawsMail/PasswordStore.pm59
-rw-r--r--lib/Config/ClawsMail/PasswordStoreParser.pm21
-rw-r--r--lib/Config/ClawsMail/Server.pm28
-rw-r--r--t/send.t29
10 files changed, 322 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index b7e23ed..12ae430 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ MANIFEST.bak
.prove
*~
/.build/
+/_Inline/
+/.inline/
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..90be09f
--- /dev/null
+++ b/Changes
@@ -0,0 +1,2 @@
+{{$NEXT}}
+ - first version
diff --git a/dist.ini b/dist.ini
index 284b5ce..2778d8b 100644
--- a/dist.ini
+++ b/dist.ini
@@ -1,7 +1,8 @@
+name = Config-ClawsMail
author = Gianni Ceccarelli <dakkar@thenautilus.net>
license = Perl_5
copyright_holder = Gianni Ceccarelli <dakkar@thenautilus.net>
-copyright_year = 2021
+copyright_year = 2023
[GatherDir]
@@ -57,8 +58,6 @@ dir = scripts
[ShareDir]
-[MakeMaker]
-
[Manifest]
[License]
diff --git a/lib/Config/ClawsMail.pm b/lib/Config/ClawsMail.pm
new file mode 100644
index 0000000..3ad6ab2
--- /dev/null
+++ b/lib/Config/ClawsMail.pm
@@ -0,0 +1,78 @@
+package Config::ClawsMail;
+use v5.26;
+use Moo;
+# VERSION
+use experimental 'signatures';
+
+use Config::ClawsMail::Account;
+use Config::ClawsMail::PasswordStore;
+use Config::ClawsMail::MainConfigParser;
+use Config::ClawsMail::PasswordStoreParser;
+use Types::Standard qw(HashRef InstanceOf);
+use Types::Path::Tiny qw(Path);
+use namespace::clean;
+
+# ABSTRACT: Claws-Mail config parser
+
+has basedir => (
+ is => 'ro',
+ isa => Path,
+ coerce => 1,
+ default => '~/.claws-mail',
+);
+
+has master_password => ( is => 'ro' );
+
+has main_config => (
+ is => 'lazy',
+ isa => HashRef,
+);
+
+sub _build_main_config($self) {
+ Config::ClawsMail::MainConfigParser->read_file($self->basedir->child('clawsrc'));
+}
+
+has password_store => (
+ is => 'lazy',
+ isa => InstanceOf['Config::ClawsMail::PasswordStore'],
+);
+
+sub _build_password_store($self) {
+ my $raw_data = Config::ClawsMail::PasswordStoreParser->read_file(
+ $self->basedir->child('passwordstorerc'),
+ );
+
+ Config::ClawsMail::PasswordStore->new({
+ raw_data => $raw_data,
+ master_password => $self->master_password,
+ master_salt_bs64 => $self->main_config->{Common}{master_passphrase_salt},
+ });
+}
+
+has accounts => (
+ is => 'lazy',
+ isa => HashRef[InstanceOf['Config::ClawsMail::Account']],
+);
+
+sub _build_accounts($self) {
+ my $raw_accounts = Config::INI::Reader->read_file(
+ $self->basedir->child('accountrc'),
+ );
+
+ my %accounts;
+
+ for my $account_id (keys $raw_accounts->%*) {
+
+ my $account = Config::ClawsMail::Account->new_from_config({
+ account_section => $account_id,
+ account_config => $raw_accounts->{$account_id},
+ password_store => $self->password_store,
+ }) or next;
+
+ $accounts{ $account->account_name } = $account;
+ }
+
+ return \%accounts;
+}
+
+1;
diff --git a/lib/Config/ClawsMail/Account.pm b/lib/Config/ClawsMail/Account.pm
new file mode 100644
index 0000000..67fc707
--- /dev/null
+++ b/lib/Config/ClawsMail/Account.pm
@@ -0,0 +1,86 @@
+package Config::ClawsMail::Account;
+use v5.26;
+use Moo;
+# VERSION
+use Types::Standard qw(Str InstanceOf);
+use Config::ClawsMail::Server;
+use namespace::clean;
+
+# ABSTRACT: Claws-Mail account
+
+has [qw(account_name name address)] => (
+ is => 'ro',
+ required => 1,
+ isa => Str,
+);
+
+has [qw(imap smtp)] => (
+ is => 'ro',
+ isa => InstanceOf['Config::ClawsMail::Server'],
+);
+
+my @ssl_string=qw(no ssl starttls);
+sub new_from_config {
+ my ($class,$args) = @_;
+
+ my $config = $args->{account_config};
+ return unless $config->{protocol} eq '1';
+
+ my $section = $args->{account_section};
+
+ my $password_store = $args->{password_store};
+
+ my $imap_server = Config::ClawsMail::Server->new({
+ host => $config->{receive_server},
+ port => (
+ $config->{set_imapport}
+ ? $config->{imap_port}
+ : $config->{ssl_imap} == 1
+ ? scalar getservbyname('imaps','tcp')
+ : scalar getservbyname('imap','tcp')
+ ),
+ ssl => $ssl_string[$config->{ssl_imap}],
+ %{$config}{qw(user_id)},
+ password => $password_store->password_for($section,'recv'),
+ });
+
+ my $smtp_server = Config::ClawsMail::Server->new({
+ host => $config->{smtp_server}||$config->{receive_server},
+ port => (
+ $config->{set_smtpport}
+ ? $config->{smtp_port}
+ : $config->{ssl_smtp} == 1
+ ? scalar getservbyname('smtps','tcp')
+ : scalar getservbyname('smtp','tcp')
+ ),
+ ssl => $ssl_string[$config->{ssl_smtp}],
+ ( $config->{use_smtp_auth} ? (
+ user_id => $config->{smtp_user_id} || $config->{user_id},
+ password => $password_store->password_for($section,'send') || $password_store->password_for($section,'recv')
+ ) : () ),
+ });
+
+ return $class->new({
+ %{$config}{qw(account_name name address)},
+ imap => $imap_server,
+ smtp => $smtp_server,
+ });
+}
+
+sub email_transport {
+ my ($self) = @_;
+
+ require Email::Sender::Transport::SMTPS;
+ my $smtp = $self->smtp;
+ return Email::Sender::Transport::SMTPS->new(
+ host => $smtp->host,
+ port => $smtp->port,
+ ssl => $smtp->ssl,
+ ( $smtp->user_id ? (
+ sasl_username => $smtp->user_id,
+ sasl_password => $smtp->password,
+ ) : () )
+ );
+}
+
+1;
diff --git a/lib/Config/ClawsMail/MainConfigParser.pm b/lib/Config/ClawsMail/MainConfigParser.pm
new file mode 100644
index 0000000..4283e05
--- /dev/null
+++ b/lib/Config/ClawsMail/MainConfigParser.pm
@@ -0,0 +1,15 @@
+package Config::ClawsMail::MainConfigParser;
+use v5.26;
+use strict;
+use warnings;
+# VERSION
+use parent 'Config::INI::Reader';
+# ABSTRACT: Config::INI::Reader tweaked for clawsrc
+
+sub handle_unparsed_line {
+ my ($self, $line, $handle) = @_;
+ return if $line =~ m{\.so$}; # plugin name
+ return $self->next::method($line,$handle);
+}
+
+1;
diff --git a/lib/Config/ClawsMail/PasswordStore.pm b/lib/Config/ClawsMail/PasswordStore.pm
new file mode 100644
index 0000000..52a0a67
--- /dev/null
+++ b/lib/Config/ClawsMail/PasswordStore.pm
@@ -0,0 +1,59 @@
+package Config::ClawsMail::PasswordStore;
+use v5.26;
+use Moo;
+# VERSION
+use experimental 'signatures';
+use Types::Standard qw(HashRef);
+use Crypt::Misc qw(decode_b64);
+use Crypt::KeyDerivation qw(pbkdf2);
+use Crypt::Cipher::AES;
+use Crypt::Mode::CBC;
+use namespace::clean;
+
+# ABSTRACT: decrypt Claws-Mail password store
+
+sub PASSCRYPT_KEY() { 'passkey0' }
+
+has raw_data => (
+ is => 'ro',
+ required => 1,
+ isa => HashRef,
+);
+
+has master_password => ( is => 'ro' );
+
+has master_salt_bs64 => ( is => 'ro', required => 1 );
+has master_salt => ( is => 'lazy' );
+sub _build_master_salt($self) { decode_b64($self->master_salt_bs64) }
+
+sub decrypt($self,$input) {
+ return $input unless $input && $input =~ m{\A \{ ([a-z0-9-]+),(\d+) \} (.+) \z}smxi;
+ my ($algo,$rounds,$ciphertext) = ($1,$2,$3);
+
+ die 'unknown algo' unless $algo eq 'AES-256-CBC';
+
+ my $key = pbkdf2(
+ $self->master_password || PASSCRYPT_KEY(),
+ $self->master_salt,
+ $rounds, 'SHA1', 32, # 32 bytes = 256 bits,for AES-256
+ );
+
+ my $cipher = Crypt::Mode::CBC->new('AES', 0); # 0 = no padding
+
+ $ciphertext = decode_b64($ciphertext);
+
+ # claws sets up 16 random bytes as IV??
+ my $iv = '0123456789abcdef';
+
+ my $cleartext = $cipher->decrypt($ciphertext, $key, $iv);
+
+ # the first 16 bytes are generated from the IV, we don't care
+ # about them
+ return substr($cleartext,16);
+}
+
+sub password_for($self,$section,$key) {
+ return $self->decrypt($self->raw_data->{$section}{$key});
+}
+
+1;
diff --git a/lib/Config/ClawsMail/PasswordStoreParser.pm b/lib/Config/ClawsMail/PasswordStoreParser.pm
new file mode 100644
index 0000000..fe76b1e
--- /dev/null
+++ b/lib/Config/ClawsMail/PasswordStoreParser.pm
@@ -0,0 +1,21 @@
+package Config::ClawsMail::PasswordStoreParser;
+use v5.26;
+use strict;
+use warnings;
+# VERSION
+use parent 'Config::INI::Reader';
+# ABSTRACT: Config::INI::Reader tweaked for passwordstorerc
+
+sub parse_section_header {
+ my ($head) = $_[1] =~ /^\s*\[\s*(.+?)\s*\]\s*$/
+ or return;
+ $head =~ s{account:}{Account: };
+ return $head;
+}
+
+sub parse_value_assignment {
+ return ($1, $2) if $_[1] =~ /^\s*([^\s\pC]+?)\s+(.*?)\s*$/;
+ return;
+}
+
+1;
diff --git a/lib/Config/ClawsMail/Server.pm b/lib/Config/ClawsMail/Server.pm
new file mode 100644
index 0000000..e7c64bb
--- /dev/null
+++ b/lib/Config/ClawsMail/Server.pm
@@ -0,0 +1,28 @@
+package Config::ClawsMail::Server;
+use v5.26;
+use Moo;
+# VERSION
+use Types::Standard qw(Str Enum);
+use Config::ClawsMail::Password;
+use namespace::clean;
+
+# ABSTRACT: Claws-Mail send/receive server
+
+has [qw(host port)] => (
+ is => 'ro',
+ required => 1,
+ isa => Str,
+);
+
+has [qw(user_id password)] => (
+ is => 'ro',
+ isa => Str,
+);
+
+has ssl => (
+ is => 'ro',
+ isa => Enum[qw(no ssl starttls)],
+ default => sub { 'no' },
+);
+
+1;
diff --git a/t/send.t b/t/send.t
new file mode 100644
index 0000000..69d21a8
--- /dev/null
+++ b/t/send.t
@@ -0,0 +1,29 @@
+#!perl
+use strict;
+use warnings;
+use Test::More;
+use Config::ClawsMail;
+use Email::Sender::Simple qw(sendmail);
+use Email::Simple;
+use Email::Simple::Creator;
+
+my $claws = Config::ClawsMail->new();
+my $account = $claws->accounts->{BB};
+my $address = sprintf q{%s <%s>},
+ $account->name, $account->address;
+
+my $email = Email::Simple->create(
+ header => [
+ To => $address,
+ From => $address,
+ Subject => 'config::claws-mail test',
+ ],
+ body => "test for Config::ClawsMail\n",
+);
+ok(
+ sendmail($email, {transport => $account->email_transport}),
+ 'sending should work',
+);
+
+done_testing;
+