diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Changes | 2 | ||||
-rw-r--r-- | dist.ini | 5 | ||||
-rw-r--r-- | lib/Config/ClawsMail.pm | 78 | ||||
-rw-r--r-- | lib/Config/ClawsMail/Account.pm | 86 | ||||
-rw-r--r-- | lib/Config/ClawsMail/MainConfigParser.pm | 15 | ||||
-rw-r--r-- | lib/Config/ClawsMail/PasswordStore.pm | 59 | ||||
-rw-r--r-- | lib/Config/ClawsMail/PasswordStoreParser.pm | 21 | ||||
-rw-r--r-- | lib/Config/ClawsMail/Server.pm | 28 | ||||
-rw-r--r-- | t/send.t | 29 |
10 files changed, 322 insertions, 3 deletions
@@ -11,3 +11,5 @@ MANIFEST.bak .prove *~ /.build/ +/_Inline/ +/.inline/ @@ -0,0 +1,2 @@ +{{$NEXT}} + - first version @@ -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; + |