Guest User

Untitled

a guest
Oct 22nd, 2018
169
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.43 KB | None | 0 0
  1. =head1 NAME
  2.  
  3. forwarding - plugin to implement DB-based E-mail forwarding
  4.  
  5. =head1 SYNOPSIS
  6.  
  7. # in config/plugins
  8. forwarding [OPTION value]...
  9.  
  10. =cut
  11.  
  12. use DBI;
  13. use Net::SMTP;
  14. use Mail::Address;
  15. use Cache::Memcached::Fast;
  16. use Qpsmtpd::Constants;
  17.  
  18. our %defaults = (
  19. DATABASE => 'my_db_name',
  20. DB_USER => 'username',
  21. DB_PASS => 'password',
  22. MAIL_SERVER => 'out.smtp.example.com',
  23. MAIL_PORT => 2525,
  24. MEMCACHED_SERVER => 'localhost:2526',
  25. MEMCACHED_NAMESPACE => 'aliases'
  26. );
  27.  
  28. sub register {
  29. my ($self, $qp, @args) = @_;
  30.  
  31. %{$self->{_args}} = ( %defaults, @args );
  32.  
  33. $self->{_memcache} = new Cache::Memcached::Fast {
  34. servers => [ $self->{_args}{MEMCACHED_SERVER} ],
  35. namespace => $self->{_args}{MEMCACHED_NAMESPACE}
  36. };
  37.  
  38. repopulate_cache( $self );
  39. $self->{_cache_version} = $self->{_memcache}->get( '_version' );
  40.  
  41. }
  42.  
  43. sub hook_pre_connection {
  44. my $self = shift;
  45.  
  46. my $version = $self->{_memcache}->get( '_version' );
  47. if ( $self->{_cache_version} != $version ) {
  48. repopulate_cache( $self ) or return (DENYSOFT, "Couldn't populate alias cache");
  49. $self->{_cache_version} = $version;
  50. }
  51.  
  52. return DECLINED;
  53. }
  54.  
  55. sub repopulate_cache {
  56. my $self = shift;
  57.  
  58. $self->log( LOGNOTICE, "Updating the alias cache" );
  59.  
  60. $self->{_address_cache} = {
  61. bounce => undef,
  62. webmaster => 'Webmaster@example.com',
  63. admin => 'mail-admin@example.com'
  64. };
  65.  
  66. my $dbh = DBI->connect( 'dbi:Pg:dbname=' . $self->{_args}{DATABASE}, DB_USER, DB_PASS )
  67. or warn "Can't connect to DB" and return;
  68.  
  69. ( my $sth = $dbh->prepare( << '' ) )->execute;
  70. SELECT e_mail_alias, '_GROUP' FROM groups WHERE e_mail_alias IS NOT NULL
  71. UNION
  72. SELECT forward_username, forward_to FROM users WHERE forward_username IS NOT NULL
  73.  
  74. $sth->bind_columns( \my($alias, $address) );
  75. while ( $sth->fetch ) {
  76. for ( split /,\s*/, $alias ) {
  77. $self->log( LOGDEBUG, "Caching $_ -> $address" );
  78. $self->{_address_cache}->{ lc $_ } = $address;
  79. }
  80. }
  81.  
  82. $dbh->disconnect;
  83.  
  84. return length keys %{$self->{_address_cache}};
  85. }
  86.  
  87. sub hook_mail {
  88. my ($self, $transaction, $sender) = @_;
  89.  
  90. return (DENY, "There's no such user here") if $sender->host
  91. and $sender->host eq 'example.com'
  92. and ! exists $self->{_address_cache}->{ lc $sender->user };
  93.  
  94. my $c = $self->qp->connection;
  95. my $message = { sender => $sender };
  96. my $messages = $c->notes( 'mg_messages' );
  97. push @$messages, $message;
  98. $c->notes( mg_messages => $messages );
  99. $c->notes( mg_current_message => $message );
  100.  
  101. return DECLINED;
  102. }
  103.  
  104. sub hook_rcpt {
  105. my ($self, $transaction, $rcpt) = @_;
  106.  
  107. my $to_user = lc $rcpt->user;
  108.  
  109. $to_user =~ s/\+.*//; # Handle plus addressing
  110.  
  111. return DECLINED if $to_user =~ m/^ical_/;
  112.  
  113. $self->log( LOGDEBUG, "Looking up $to_user" );
  114.  
  115. # Get group or forwarding record, or deny message if there isn't one
  116.  
  117. my $destination = $self->{_address_cache}->{ lc $to_user };
  118. my $message = $self->qp->connection->notes('mg_current_message');
  119.  
  120. if ( ! $destination ) {
  121.  
  122. $self->log( LOGNOTICE, sprintf 'Delivery denied (address %s not found)', $rcpt->address );
  123. return ( DENY, sprintf 'Address %s not found', $rcpt->address );
  124.  
  125. } elsif ( $destination eq '_GROUP' ) {
  126.  
  127. my $groups = $message->{groups} ||= [];
  128. push @$groups, $rcpt;
  129. return OK;
  130.  
  131. } elsif ( defined $destination ) {
  132.  
  133. my $users = $message->{users} ||= [];
  134. push @$users, split /,\s*/, $destination;
  135. return OK;
  136.  
  137. } else {
  138.  
  139. return DECLINED;
  140.  
  141. }
  142. }
  143.  
  144. sub hook_data_post {
  145. my ($self, $transaction) = @_;
  146.  
  147. my $headers = $transaction->header;
  148.  
  149. return DECLINED if $headers->get('To') =~ m/ical_.*\+/;
  150.  
  151. my $x_loop = $headers->get('X-Loop') || '';
  152. if ( $x_loop eq "bounce\@example.com" ) {
  153. return ( DENY, 'Got message with my X-Loop header' );
  154. }
  155.  
  156. my $subject = $headers->get('Subject');
  157. if ( $subject =~ m{out of office|autoreply}i ) {
  158. return ( DENY, 'Dumping vacation autoreply' );
  159. }
  160.  
  161. my $from = (Mail::Address->parse( $headers->get('From') ))[0]->address;
  162.  
  163. my $message = $self->qp->connection->notes('mg_current_message');
  164.  
  165. my $dbh;
  166.  
  167. for my $rcpt ( @{ $message->{groups} } ) {
  168.  
  169. my $to_user = lc $rcpt->user;
  170.  
  171. $dbh ||= DBI->connect( 'dbi:Pg:dbname=' . $self->{_args}{DATABASE}, DB_USER, DB_PASS )
  172. or return (DENYSOFT_DISCONNECT, "Couldn't connect to database");
  173.  
  174. my $group = $dbh->selectrow_hashref( << '', {}, $to_user );
  175. SELECT group_id, name, e_mail_alias, e_mail_to_alias_allowed_from, custom_e_mail_query, log_all_recipients
  176. FROM groups WHERE lower(e_mail_alias) = ?
  177.  
  178. my $group_error = sub {
  179. delete $message->{groups};
  180. return (
  181. DENY,
  182. sprintf 'The group "%s" <%s> only accepts mail from %s; message from %s rejected',
  183. $group->{name}, $rcpt->address, $_[0], $from
  184. );
  185. };
  186.  
  187. if (
  188. ! $group->{e_mail_to_alias_allowed_from}
  189. or $group->{e_mail_to_alias_allowed_from} eq 'nobody'
  190. ) {
  191. return $group_error->(
  192. sprintf 'The group "%s" <%s> does not accept mail', $group->{name}, $rcpt->address
  193. );
  194. }
  195.  
  196. if ( $group->{e_mail_to_alias_allowed_from} ne 'anybody' ) {
  197. # is there a user record with the sender's e-mail address?
  198.  
  199. my $user ||= $dbh->selectrow_hashref( << '', {}, lc $from, lc $from )
  200. SELECT user_id FROM users WHERE lower(e_mail) = ? OR lower(alternate_from_e_mail) = ?
  201.  
  202. or return $group_error->( 'registered users of the Web site' );
  203.  
  204. if ( $group->{e_mail_to_alias_allowed_from} eq 'admins' ) {
  205. # has the user been accepted as a member of the 'Web-site admins' group?
  206.  
  207. $dbh->selectrow_array( << '', {}, $user->{user_id} )
  208. SELECT 1 FROM user_in_group JOIN groups USING (group_id)
  209. WHERE user_id = ? AND groups.name = 'Web-site admins' AND is_accepted IS TRUE
  210.  
  211. or return $group_error->( 'Web-site admins' );
  212.  
  213. } elsif ( $group->{e_mail_to_alias_allowed_from} eq 'group' ) {
  214. # has the user been accepted as a member of this group?
  215.  
  216. $dbh->selectrow_array( << '', {}, $user->{user_id}, $group->{group_id} )
  217. SELECT 1 FROM user_in_group
  218. WHERE user_id = ? AND group_id = ? AND is_accepted IS TRUE
  219.  
  220. or return $group_error->( 'its members' );
  221.  
  222. } elsif ( $group->{e_mail_to_alias_allowed_from} eq 'group_admins' ) {
  223. # is the user an administrator of this group?
  224.  
  225. $dbh->selectrow_array( << '', {}, $user->{user_id}, $group->{group_id} )
  226. SELECT 1 FROM user_in_group
  227. WHERE user_id = ? AND group_id = ? AND is_accepted IS TRUE AND is_admin IS TRUE
  228.  
  229. or return $group_error->( 'its administrators' );
  230.  
  231. } elsif ( $group->{e_mail_to_alias_allowed_from} eq 'members' ) {
  232. # has the user been accepted as a member of a member group?
  233.  
  234. $dbh->selectrow_array( << '', {}, $user->{user_id} )
  235. SELECT 1 FROM user_in_group JOIN groups USING (group_id)
  236. WHERE user_id = ? AND is_accepted IS TRUE AND membership_type_id IS NOT NULL
  237. AND groups.is_suspended IS NOT TRUE
  238.  
  239. or return $group_error->( 'users associated with member firms' );
  240.  
  241. }
  242.  
  243. }
  244.  
  245. $group->{rcpt} = $rcpt; # Keep a link to the rcpt object in the group hash
  246.  
  247. $rcpt = $group; # Replace the rcpt objects in the connection note with group hashes
  248.  
  249. }
  250.  
  251. $self->qp->connection->notes( mg_dbh => $dbh ) if $dbh;
  252.  
  253. if ( $message->{groups} || $message->{users} ) {
  254.  
  255. my %desired_tags = map { $_ => 1 } qw(
  256. From To CC Bcc Subject Date Content-Type Content-Transfer-Encoding
  257. MIME-Version List-Id X-Loop
  258. );
  259. $headers->delete( $_ ) for grep ! $desired_tags{$_}, $headers->tags;
  260.  
  261. $message->{body} = $transaction->body_as_string;
  262. $message->{from} = $from;
  263. $message->{headers} = $headers;
  264.  
  265. }
  266.  
  267. return DECLINED;
  268. }
  269.  
  270. sub hook_queue {
  271. my ($self, $transaction) = @_;
  272.  
  273. my $message = $self->qp->connection->notes('mg_current_message');
  274.  
  275. if ( $message->{groups} || $message->{users} ) {
  276. return OK;
  277. } else {
  278. return DECLINED;
  279. }
  280. }
  281.  
  282. sub hook_post_connection {
  283. my ($self) = @_;
  284.  
  285. my $dbh;
  286.  
  287. my $messages = $self->qp->connection->notes('mg_messages') or return DECLINED;
  288.  
  289. for my $message ( @$messages ) {
  290.  
  291. next unless ( $message->{groups} || $message->{users} ) && $message->{body};
  292.  
  293. my $headers = $message->{headers};
  294. $headers->replace( 'X-Original-Sender', $message->{sender} );
  295. my $subject = $headers->get('Subject');
  296.  
  297. my $smtp = Net::SMTP->new(
  298. Host => $self->{_args}{MAIL_SERVER}, Port => $self->{_args}{MAIL_PORT}, Timeout => 10, Debug => 0
  299. ) or $self->log( LOGCRIT, "Timeout connecting to SMTP server - message not mailed" );
  300.  
  301. for my $address ( @{ $message->{users} } ) {
  302.  
  303. $self->log(LOGDEBUG, $headers->as_string);
  304.  
  305. $self->log( LOGNOTICE,
  306. sprintf 'Forwarding mail from %s to %s', $message->{from}, $address
  307. );
  308.  
  309. $smtp->mail( "bounce\@example.com" );
  310. $smtp->to( $address );
  311. $smtp->data;
  312. $smtp->datasend( $headers->as_string );
  313. $smtp->datasend( $message->{body} );
  314. $smtp->dataend;
  315.  
  316. }
  317.  
  318. for my $group ( @{ $message->{groups} } ) {
  319.  
  320. $headers->replace( 'List-Id', $group->{rcpt}->address );
  321. $headers->replace( 'X-Loop', "bounce\@example.com" );
  322.  
  323. $subject =~ s/\[[^\]]+\] //;
  324. $subject = sprintf '[%s] %s', $group->{rcpt}->address, $subject;
  325. $headers->replace( 'Subject', $subject );
  326.  
  327. $self->log( LOGDEBUG, $headers->as_string );
  328.  
  329. $self->log( LOGNOTICE,
  330. sprintf 'Forwarding mail from %s to group alias "%s"',
  331. $message->{from}, $group->{e_mail_alias}
  332. );
  333.  
  334. $dbh ||= $self->qp->connection->notes('mg_dbh')
  335. || DBI->connect( 'dbi:Pg:dbname=' . $self->{_args}{DATABASE}, DB_USER, DB_PASS )
  336. or $self->log( LOGCRIT, "Couldn't connect to database" ) && next;
  337.  
  338. $dbh->begin_work;
  339.  
  340. if ( $group->{custom_e_mail_query} ) {
  341.  
  342. $dbh->prepare( 'DECLARE csr1 CURSOR FOR ' . $group->{custom_e_mail_query} )->execute;
  343.  
  344. } else {
  345.  
  346. $dbh->prepare( << '' )->execute( $group->{group_id} );
  347. DECLARE csr1 CURSOR FOR
  348. SELECT user_id, first_name, last_name, e_mail
  349. FROM user_in_group JOIN users USING (user_id)
  350. WHERE group_id = ? AND is_accepted IS TRUE
  351. AND is_suspended IS NOT TRUE AND bounced_mail IS NOT TRUE
  352.  
  353. }
  354.  
  355. BLOCK: while ( ( my $sth = $dbh->prepare( 'FETCH 500 FROM csr1' ) )->execute > 0 ) {
  356.  
  357. $smtp->mail( "bounce\@example.com" );
  358.  
  359. ROW: while ( my ($user_id, $first_name, $last_name, $e_mail) = $sth->fetchrow ) {
  360. next unless $e_mail;
  361. my ($user, $domain) = split '@', $e_mail, 2;
  362. if (
  363. $domain eq 'example.com' and
  364. my $alias = $self->{_address_cache}->{ lc((split '\+', $user)[0]) }
  365. ) {
  366. $self->log( LOGDEBUG, "$e_mail => $alias" );
  367. $e_mail = $alias unless $alias eq '_GROUP';
  368. }
  369. $self->log( LOGDEBUG, "Forwarding to $e_mail" ) if $group->{log_all_recipients};
  370. $smtp->to( $e_mail );
  371. }
  372.  
  373. $smtp->data;
  374. $smtp->datasend( $headers->as_string );
  375. $smtp->datasend( $message->{body} );
  376. $smtp->dataend;
  377.  
  378. }
  379.  
  380. $dbh->commit;
  381.  
  382. }
  383.  
  384. }
  385.  
  386. return OK;
  387. }
Add Comment
Please, Sign In to add comment