mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
email-mirror: Remove HTTP interface.
This commit is contained in:
committed by
Tim Abbott
parent
a6a5fc246a
commit
c6e0f0b436
@@ -18,7 +18,7 @@ that address will be delivered into the channel.
|
|||||||
|
|
||||||
There are two ways to configure Zulip's email gateway:
|
There are two ways to configure Zulip's email gateway:
|
||||||
|
|
||||||
1. Local delivery (recommended): A postfix server runs on the Zulip
|
1. Local delivery (recommended): A server runs on the Zulip
|
||||||
server and passes the emails directly to Zulip.
|
server and passes the emails directly to Zulip.
|
||||||
1. Polling: A cron job running on the Zulip server checks an IMAP
|
1. Polling: A cron job running on the Zulip server checks an IMAP
|
||||||
inbox (`username@example.com`) every minute for new emails.
|
inbox (`username@example.com`) every minute for new emails.
|
||||||
@@ -68,27 +68,13 @@ using an [HTTP reverse proxy][reverse-proxy]).
|
|||||||
|
|
||||||
1. Log in to your Zulip server; the remaining steps all happen there.
|
1. Log in to your Zulip server; the remaining steps all happen there.
|
||||||
|
|
||||||
1. Add `, zulip::postfix_localmail` to `puppet_classes` in
|
1. Add `, zulip::local_mailserver` to `puppet_classes` in
|
||||||
`/etc/zulip/zulip.conf`. A typical value after this change is:
|
`/etc/zulip/zulip.conf`. A typical value after this change is:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
puppet_classes = zulip::profile::standalone, zulip::postfix_localmail
|
puppet_classes = zulip::profile::standalone, zulip::local_mailserver
|
||||||
```
|
```
|
||||||
|
|
||||||
1. If `hostname.example.com` is different from
|
|
||||||
`emaildomain.example.com`, add a section to `/etc/zulip/zulip.conf`
|
|
||||||
on your Zulip server like this:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[postfix]
|
|
||||||
mailname = emaildomain.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells postfix to expect to receive emails at addresses ending with
|
|
||||||
`@emaildomain.example.com`, overriding the default of
|
|
||||||
`@hostname.example.com`. It will also identify itself as
|
|
||||||
`emaildomain.example.com` on any outgoing emails it sends.
|
|
||||||
|
|
||||||
1. Run `/home/zulip/deployments/current/scripts/zulip-puppet-apply`
|
1. Run `/home/zulip/deployments/current/scripts/zulip-puppet-apply`
|
||||||
(and answer `y`) to apply your new `/etc/zulip/zulip.conf`
|
(and answer `y`) to apply your new `/etc/zulip/zulip.conf`
|
||||||
configuration to your Zulip server.
|
configuration to your Zulip server.
|
||||||
|
@@ -221,14 +221,6 @@ the message to fail to send.
|
|||||||
|
|
||||||
Set to the port number for the KaTeX server; defaults to port 9700.
|
Set to the port number for the KaTeX server; defaults to port 9700.
|
||||||
|
|
||||||
### `[postfix]`
|
|
||||||
|
|
||||||
#### `mailname`
|
|
||||||
|
|
||||||
The hostname that [Postfix should be configured to receive mail
|
|
||||||
at](email-gateway.md#local-delivery-setup), as well as identify itself as for
|
|
||||||
outgoing email.
|
|
||||||
|
|
||||||
### `[postgresql]`
|
### `[postgresql]`
|
||||||
|
|
||||||
#### `effective_io_concurrency`
|
#### `effective_io_concurrency`
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
# This is the list of email addresses that are accepted via SMTP;
|
|
||||||
# these consist of only the addresses in `virtual`, as well as the
|
|
||||||
# RFC822-specified postmaster.
|
|
||||||
|
|
||||||
/\+.*@/ OK
|
|
||||||
/\..*@/ OK
|
|
||||||
/^mm.{32}@/ OK
|
|
||||||
|
|
||||||
/^postmaster@/ OK
|
|
@@ -1,114 +0,0 @@
|
|||||||
#
|
|
||||||
# Postfix master process configuration file. For details on the format
|
|
||||||
# of the file, see the master(5) manual page (command: "man 5 master").
|
|
||||||
#
|
|
||||||
# Do not forget to execute "postfix reload" after editing this file.
|
|
||||||
#
|
|
||||||
# ==========================================================================
|
|
||||||
# service type private unpriv chroot wakeup maxproc command + args
|
|
||||||
# (yes) (yes) (yes) (never) (100)
|
|
||||||
# ==========================================================================
|
|
||||||
smtp inet n - - - - smtpd
|
|
||||||
#smtp inet n - - - 1 postscreen
|
|
||||||
#smtpd pass - - - - - smtpd
|
|
||||||
#dnsblog unix - - - - 0 dnsblog
|
|
||||||
#tlsproxy unix - - - - 0 tlsproxy
|
|
||||||
#submission inet n - - - - smtpd
|
|
||||||
# -o syslog_name=postfix/submission
|
|
||||||
# -o smtpd_tls_security_level=encrypt
|
|
||||||
# -o smtpd_sasl_auth_enable=yes
|
|
||||||
# -o smtpd_client_restrictions=permit_sasl_authenticated,reject
|
|
||||||
# -o milter_macro_daemon_name=ORIGINATING
|
|
||||||
#smtps inet n - - - - smtpd
|
|
||||||
# -o syslog_name=postfix/smtps
|
|
||||||
# -o smtpd_tls_wrappermode=yes
|
|
||||||
# -o smtpd_sasl_auth_enable=yes
|
|
||||||
# -o smtpd_client_restrictions=permit_sasl_authenticated,reject
|
|
||||||
# -o milter_macro_daemon_name=ORIGINATING
|
|
||||||
#628 inet n - - - - qmqpd
|
|
||||||
pickup fifo n - - 60 1 pickup
|
|
||||||
cleanup unix n - - - 0 cleanup
|
|
||||||
qmgr fifo n - n 300 1 qmgr
|
|
||||||
#qmgr fifo n - n 300 1 oqmgr
|
|
||||||
tlsmgr unix - - - 1000? 1 tlsmgr
|
|
||||||
rewrite unix - - - - - trivial-rewrite
|
|
||||||
bounce unix - - - - 0 bounce
|
|
||||||
defer unix - - - - 0 bounce
|
|
||||||
trace unix - - - - 0 bounce
|
|
||||||
verify unix - - - - 1 verify
|
|
||||||
flush unix n - - 1000? 0 flush
|
|
||||||
proxymap unix - - n - - proxymap
|
|
||||||
proxywrite unix - - n - 1 proxymap
|
|
||||||
smtp unix - - - - - smtp
|
|
||||||
relay unix - - - - - smtp
|
|
||||||
# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
|
|
||||||
showq unix n - - - - showq
|
|
||||||
error unix - - - - - error
|
|
||||||
retry unix - - - - - error
|
|
||||||
discard unix - - - - - discard
|
|
||||||
local unix - n n - - local
|
|
||||||
virtual unix - n n - - virtual
|
|
||||||
lmtp unix - - - - - lmtp
|
|
||||||
anvil unix - - - - 1 anvil
|
|
||||||
scache unix - - - - 1 scache
|
|
||||||
#
|
|
||||||
# ====================================================================
|
|
||||||
# Interfaces to non-Postfix software. Be sure to examine the manual
|
|
||||||
# pages of the non-Postfix software to find out what options it wants.
|
|
||||||
#
|
|
||||||
# Many of the following services use the Postfix pipe(8) delivery
|
|
||||||
# agent. See the pipe(8) man page for information about ${recipient}
|
|
||||||
# and other message envelope options.
|
|
||||||
# ====================================================================
|
|
||||||
#
|
|
||||||
# maildrop. See the Postfix MAILDROP_README file for details.
|
|
||||||
# Also specify in main.cf: maildrop_destination_recipient_limit=1
|
|
||||||
#
|
|
||||||
maildrop unix - n n - - pipe
|
|
||||||
flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
|
|
||||||
#
|
|
||||||
# ====================================================================
|
|
||||||
#
|
|
||||||
# Recent Cyrus versions can use the existing "lmtp" master.cf entry.
|
|
||||||
#
|
|
||||||
# Specify in cyrus.conf:
|
|
||||||
# lmtp cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
|
|
||||||
#
|
|
||||||
# Specify in main.cf one or more of the following:
|
|
||||||
# mailbox_transport = lmtp:inet:localhost
|
|
||||||
# virtual_transport = lmtp:inet:localhost
|
|
||||||
#
|
|
||||||
# ====================================================================
|
|
||||||
#
|
|
||||||
# Cyrus 2.1.5 (Amos Gouaux)
|
|
||||||
# Also specify in main.cf: cyrus_destination_recipient_limit=1
|
|
||||||
#
|
|
||||||
#cyrus unix - n n - - pipe
|
|
||||||
# user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user}
|
|
||||||
#
|
|
||||||
# ====================================================================
|
|
||||||
# Old example of delivery via Cyrus.
|
|
||||||
#
|
|
||||||
#old-cyrus unix - n n - - pipe
|
|
||||||
# flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
|
|
||||||
#
|
|
||||||
# ====================================================================
|
|
||||||
#
|
|
||||||
# See the Postfix UUCP_README file for configuration details.
|
|
||||||
#
|
|
||||||
uucp unix - n n - - pipe
|
|
||||||
flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
|
|
||||||
#
|
|
||||||
# Other external delivery methods.
|
|
||||||
#
|
|
||||||
ifmail unix - n n - - pipe
|
|
||||||
flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
|
|
||||||
bsmtp unix - n n - - pipe
|
|
||||||
flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
|
|
||||||
scalemail-backend unix - n n - 2 pipe
|
|
||||||
flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
|
|
||||||
mailman unix - n n - - pipe
|
|
||||||
flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
|
|
||||||
${nexthop} ${user}
|
|
||||||
zulip unix - n n - - pipe
|
|
||||||
flags= user=zulip argv=/home/zulip/deployments/current/scripts/lib/email-mirror-postfix -r ${original_recipient}
|
|
@@ -1 +0,0 @@
|
|||||||
zulip@localhost zulip:
|
|
@@ -1,81 +0,0 @@
|
|||||||
class zulip::postfix_localmail {
|
|
||||||
include zulip::snakeoil
|
|
||||||
$postfix_packages = [ 'postfix', ]
|
|
||||||
|
|
||||||
$fqdn = $facts['networking']['fqdn']
|
|
||||||
if $fqdn == '' {
|
|
||||||
fail('Your system does not have a fully-qualified domain name defined. See hostname(1).')
|
|
||||||
}
|
|
||||||
$postfix_mailname = zulipconf('postfix', 'mailname', $fqdn)
|
|
||||||
package { $postfix_packages:
|
|
||||||
ensure => installed,
|
|
||||||
require => File['/etc/mailname'],
|
|
||||||
}
|
|
||||||
|
|
||||||
service { 'postfix':
|
|
||||||
require => Exec['generate-default-snakeoil'],
|
|
||||||
}
|
|
||||||
|
|
||||||
file {'/etc/mailname':
|
|
||||||
ensure => file,
|
|
||||||
mode => '0644',
|
|
||||||
owner => root,
|
|
||||||
group => root,
|
|
||||||
content => $postfix_mailname,
|
|
||||||
}
|
|
||||||
|
|
||||||
file {'/etc/postfix/main.cf':
|
|
||||||
ensure => file,
|
|
||||||
mode => '0644',
|
|
||||||
owner => root,
|
|
||||||
group => root,
|
|
||||||
content => template('zulip/postfix/main.cf.erb'),
|
|
||||||
require => Package[postfix],
|
|
||||||
notify => Service['postfix'],
|
|
||||||
}
|
|
||||||
file {'/etc/postfix/master.cf':
|
|
||||||
ensure => file,
|
|
||||||
mode => '0644',
|
|
||||||
owner => root,
|
|
||||||
group => root,
|
|
||||||
source => 'puppet:///modules/zulip/postfix/master.cf',
|
|
||||||
require => Package[postfix],
|
|
||||||
notify => Service['postfix'],
|
|
||||||
}
|
|
||||||
|
|
||||||
file {'/etc/postfix/virtual':
|
|
||||||
ensure => file,
|
|
||||||
mode => '0644',
|
|
||||||
owner => root,
|
|
||||||
group => root,
|
|
||||||
content => template('zulip/postfix/virtual.erb'),
|
|
||||||
require => Package[postfix],
|
|
||||||
notify => Service['postfix'],
|
|
||||||
}
|
|
||||||
|
|
||||||
file {'/etc/postfix/transport':
|
|
||||||
ensure => file,
|
|
||||||
mode => '0644',
|
|
||||||
owner => root,
|
|
||||||
group => root,
|
|
||||||
source => 'puppet:///modules/zulip/postfix/transport',
|
|
||||||
require => Package[postfix],
|
|
||||||
}
|
|
||||||
exec {'postmap /etc/postfix/transport':
|
|
||||||
subscribe => File['/etc/postfix/transport'],
|
|
||||||
refreshonly => true,
|
|
||||||
require => [
|
|
||||||
File['/etc/postfix/main.cf'],
|
|
||||||
Package[postfix],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
file {'/etc/postfix/access':
|
|
||||||
ensure => file,
|
|
||||||
mode => '0644',
|
|
||||||
owner => root,
|
|
||||||
group => root,
|
|
||||||
source => 'puppet:///modules/zulip/postfix/access',
|
|
||||||
require => Package[postfix],
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +1,9 @@
|
|||||||
class zulip::snakeoil {
|
class zulip::snakeoil {
|
||||||
zulip::safepackage { 'ssl-cert': ensure => installed }
|
zulip::safepackage { 'ssl-cert': ensure => installed }
|
||||||
|
|
||||||
# We use the snakeoil certificate for PostgreSQL and Postfix; some VMs
|
# We use the snakeoil certificate for PostgreSQL; some VMs install
|
||||||
# install the `ssl-cert` package but (reasonably) don't build the
|
# the `ssl-cert` package but (reasonably) don't build the snakeoil
|
||||||
# snakeoil certs into the image; build them as needed.
|
# certs into the image; build them as needed.
|
||||||
exec { 'generate-default-snakeoil':
|
exec { 'generate-default-snakeoil':
|
||||||
require => Package['ssl-cert'],
|
require => Package['ssl-cert'],
|
||||||
creates => '/etc/ssl/certs/ssl-cert-snakeoil.pem',
|
creates => '/etc/ssl/certs/ssl-cert-snakeoil.pem',
|
||||||
|
@@ -1,43 +0,0 @@
|
|||||||
# This file is managed by Puppet; local changes will be overridden.
|
|
||||||
|
|
||||||
smtpd_banner = $myhostname ESMTP $mail_name (Zulip)
|
|
||||||
biff = no
|
|
||||||
|
|
||||||
# appending .domain is the MUA's job.
|
|
||||||
append_dot_mydomain = no
|
|
||||||
|
|
||||||
readme_directory = no
|
|
||||||
|
|
||||||
# TLS parameters
|
|
||||||
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
|
|
||||||
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
|
|
||||||
smtpd_use_tls=yes
|
|
||||||
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
|
|
||||||
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
|
||||||
|
|
||||||
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated reject_unauth_destination
|
|
||||||
smtpd_recipient_restrictions = permit_mynetworks, check_recipient_access regexp:/etc/postfix/access, reject
|
|
||||||
myhostname = <%= @fqdn %>
|
|
||||||
alias_maps = hash:/etc/aliases
|
|
||||||
alias_database = hash:/etc/aliases
|
|
||||||
transport_maps = hash:/etc/postfix/transport
|
|
||||||
myorigin = /etc/mailname
|
|
||||||
mydestination = localhost, <%= @postfix_mailname %>
|
|
||||||
relayhost =
|
|
||||||
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
|
|
||||||
mailbox_size_limit = 0
|
|
||||||
recipient_delimiter = +
|
|
||||||
inet_interfaces = all
|
|
||||||
inet_protocols = all
|
|
||||||
|
|
||||||
# This enables us to do a catchall
|
|
||||||
virtual_alias_maps = regexp:/etc/postfix/virtual
|
|
||||||
|
|
||||||
# Needed to accept mail with percents without trying to interpret as
|
|
||||||
# percent-style routing.
|
|
||||||
allow_percent_hack = no
|
|
||||||
allow_untrusted_routing = yes
|
|
||||||
strict_rfc821_envelopes = no
|
|
||||||
|
|
||||||
# Ingest messages up to 25MB
|
|
||||||
message_size_limit = 26214400
|
|
@@ -1,7 +0,0 @@
|
|||||||
if /@<%= Regexp.escape(@postfix_mailname) %>\.?$/
|
|
||||||
# Changes to this list require a corresponding change to `access` as
|
|
||||||
# well.
|
|
||||||
/\+.*@/ zulip@localhost
|
|
||||||
/\..*@/ zulip@localhost
|
|
||||||
/^mm.{32}@/ zulip@localhost
|
|
||||||
endif
|
|
@@ -1,169 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Postfix implementation of the incoming email gateway's helper for
|
|
||||||
forwarding emails into Zulip.
|
|
||||||
|
|
||||||
https://zulip.readthedocs.io/en/latest/production/email-gateway.html
|
|
||||||
|
|
||||||
The email gateway supports two major modes of operation: An email
|
|
||||||
server (using Postfix) where the email address configured in
|
|
||||||
EMAIL_GATEWAY_PATTERN delivers emails directly to Zulip (this) or a
|
|
||||||
cron job that connects to an IMAP inbox (which receives the emails)
|
|
||||||
periodically.
|
|
||||||
|
|
||||||
Zulip's Puppet configuration takes care of configuring Postfix to
|
|
||||||
execute this script when emails are received by Postfix, piping the
|
|
||||||
email content via standard input (and the destination email address in
|
|
||||||
the ORIGINAL_RECIPIENT environment variable).
|
|
||||||
|
|
||||||
In Postfix, you can express that via an /etc/aliases entry like this:
|
|
||||||
|/home/zulip/deployments/current/scripts/lib/email-mirror-postfix -r ${original_recipient}
|
|
||||||
|
|
||||||
To manage DoS issues, this script does very little work (just sending
|
|
||||||
an HTTP request to queue the message for processing) to avoid
|
|
||||||
importing expensive libraries.
|
|
||||||
|
|
||||||
Also you can use optional keys to configure the script and change default values:
|
|
||||||
|
|
||||||
-s SHARED_SECRET For adding shared secret key if it is not contained in
|
|
||||||
"/etc/zulip/zulip-secrets.conf". This key is used to authenticate
|
|
||||||
the HTTP requests made by this tool.
|
|
||||||
|
|
||||||
-d HOST Destination Zulip host for email uploading. Address must contain type of
|
|
||||||
HTTP protocol, e.g. "https://example.com". Default value: "https://127.0.0.1".
|
|
||||||
|
|
||||||
-u URL Destination relative for email uploading. Default value: "/api/internal/email_mirror_message".
|
|
||||||
|
|
||||||
-n Disable checking ssl certificate. This option is used for
|
|
||||||
self-signed certificates. Default value: False.
|
|
||||||
|
|
||||||
-t Disable sending request to the Zulip server. Default value: False.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import posix
|
|
||||||
import ssl
|
|
||||||
import sys
|
|
||||||
from configparser import RawConfigParser
|
|
||||||
from typing import NoReturn
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
from urllib.parse import urlencode, urljoin, urlsplit
|
|
||||||
from urllib.request import Request, urlopen
|
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
||||||
from scripts.lib.zulip_tools import get_config, get_config_file
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
parser.add_argument("-r", "--recipient", default="", help="Original recipient.")
|
|
||||||
|
|
||||||
parser.add_argument("-s", "--shared-secret", default="", help="Secret access key.")
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"-d",
|
|
||||||
"--dst-host",
|
|
||||||
dest="host",
|
|
||||||
default="127.0.0.1",
|
|
||||||
help="Destination server address for uploading email from email mirror. "
|
|
||||||
"Address must contain an HTTP protocol. Otherwise, default value is assumed "
|
|
||||||
"based on the http_only setting.",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"-u",
|
|
||||||
"--dst-url",
|
|
||||||
dest="url",
|
|
||||||
default="/api/internal/email_mirror_message",
|
|
||||||
help="Destination relative URL for uploading email from email mirror.",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"-n",
|
|
||||||
"--not-verify-ssl",
|
|
||||||
dest="verify_ssl",
|
|
||||||
action="store_false",
|
|
||||||
help="Disable ssl certificate verifying for self-signed certificates",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument("-t", "--test", action="store_true", help="Test mode.")
|
|
||||||
|
|
||||||
options = parser.parse_args()
|
|
||||||
|
|
||||||
MAX_ALLOWED_PAYLOAD = 25 * 1024 * 1024
|
|
||||||
|
|
||||||
|
|
||||||
def process_response_error(e: HTTPError) -> NoReturn:
|
|
||||||
if e.code == 400:
|
|
||||||
response_content = e.read()
|
|
||||||
response_data = json.loads(response_content)
|
|
||||||
print(response_data["msg"])
|
|
||||||
sys.exit(posix.EX_NOUSER)
|
|
||||||
else:
|
|
||||||
print("4.4.2 Connection dropped: Internal server error.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def send_email_mirror(
|
|
||||||
rcpt_to: str,
|
|
||||||
shared_secret: str,
|
|
||||||
host: str,
|
|
||||||
url: str,
|
|
||||||
test: bool,
|
|
||||||
verify_ssl: bool,
|
|
||||||
) -> None:
|
|
||||||
if not rcpt_to:
|
|
||||||
print("5.1.1 Bad destination mailbox address: No missed message email address.")
|
|
||||||
sys.exit(posix.EX_NOUSER)
|
|
||||||
msg_bytes = sys.stdin.buffer.read(MAX_ALLOWED_PAYLOAD + 1)
|
|
||||||
if len(msg_bytes) > MAX_ALLOWED_PAYLOAD:
|
|
||||||
# We're not at EOF, reject large mail.
|
|
||||||
print("5.3.4 Message too big for system: Max size is 25MiB")
|
|
||||||
sys.exit(posix.EX_DATAERR)
|
|
||||||
|
|
||||||
secrets_file = RawConfigParser()
|
|
||||||
secrets_file.read("/etc/zulip/zulip-secrets.conf")
|
|
||||||
if not shared_secret:
|
|
||||||
shared_secret = secrets_file.get("secrets", "shared_secret")
|
|
||||||
|
|
||||||
if test:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not urlsplit(host).scheme:
|
|
||||||
config_file = get_config_file()
|
|
||||||
http_only = get_config(config_file, "application_server", "http_only", False)
|
|
||||||
scheme = "http://" if http_only else "https://"
|
|
||||||
host = scheme + host
|
|
||||||
|
|
||||||
if host == "https://127.0.0.1":
|
|
||||||
# Don't try to verify SSL when posting to 127.0.0.1; it won't
|
|
||||||
# work, and connections to 127.0.0.1 are secure without SSL.
|
|
||||||
verify_ssl = False
|
|
||||||
|
|
||||||
# Because this script is run from postfix, it does not have any
|
|
||||||
# http proxy environment variables set which might interfere with
|
|
||||||
# access to localhost.
|
|
||||||
|
|
||||||
context = None
|
|
||||||
if not verify_ssl:
|
|
||||||
context = ssl.create_default_context()
|
|
||||||
context.check_hostname = False
|
|
||||||
context.verify_mode = ssl.CERT_NONE
|
|
||||||
data = {
|
|
||||||
"rcpt_to": rcpt_to,
|
|
||||||
"msg_base64": base64.b64encode(msg_bytes).decode(),
|
|
||||||
"secret": shared_secret,
|
|
||||||
}
|
|
||||||
req = Request(url=urljoin(host, url), data=urlencode(data).encode())
|
|
||||||
try:
|
|
||||||
urlopen(req, context=context)
|
|
||||||
except HTTPError as err:
|
|
||||||
process_response_error(err)
|
|
||||||
|
|
||||||
|
|
||||||
recipient = str(os.environ.get("ORIGINAL_RECIPIENT", options.recipient))
|
|
||||||
send_email_mirror(
|
|
||||||
recipient, options.shared_secret, options.host, options.url, options.test, options.verify_ssl
|
|
||||||
)
|
|
@@ -25,7 +25,6 @@ from zerver.lib.email_mirror_helpers import (
|
|||||||
from zerver.lib.email_notifications import convert_html_to_markdown
|
from zerver.lib.email_notifications import convert_html_to_markdown
|
||||||
from zerver.lib.exceptions import JsonableError, RateLimitedError
|
from zerver.lib.exceptions import JsonableError, RateLimitedError
|
||||||
from zerver.lib.message import normalize_body, truncate_content, truncate_topic
|
from zerver.lib.message import normalize_body, truncate_content, truncate_topic
|
||||||
from zerver.lib.queue import queue_json_publish_rollback_unsafe
|
|
||||||
from zerver.lib.rate_limiter import RateLimitedObject
|
from zerver.lib.rate_limiter import RateLimitedObject
|
||||||
from zerver.lib.send_email import FromAddress
|
from zerver.lib.send_email import FromAddress
|
||||||
from zerver.lib.streams import access_stream_for_send_message
|
from zerver.lib.streams import access_stream_for_send_message
|
||||||
@@ -531,28 +530,7 @@ def validate_to_address(rcpt_to: str) -> None:
|
|||||||
decode_stream_email_address(rcpt_to)
|
decode_stream_email_address(rcpt_to)
|
||||||
|
|
||||||
|
|
||||||
def mirror_email_message(rcpt_to: str, msg_base64: str) -> dict[str, str]:
|
|
||||||
try:
|
|
||||||
validate_to_address(rcpt_to)
|
|
||||||
except ZulipEmailForwardError as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"msg": f"5.1.1 Bad destination mailbox address: {e}",
|
|
||||||
}
|
|
||||||
|
|
||||||
queue_json_publish_rollback_unsafe(
|
|
||||||
"email_mirror",
|
|
||||||
{
|
|
||||||
"rcpt_to": rcpt_to,
|
|
||||||
"msg_base64": msg_base64,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return {"status": "success"}
|
|
||||||
|
|
||||||
|
|
||||||
# Email mirror rate limiter code:
|
# Email mirror rate limiter code:
|
||||||
|
|
||||||
|
|
||||||
class RateLimitedRealmMirror(RateLimitedObject):
|
class RateLimitedRealmMirror(RateLimitedObject):
|
||||||
def __init__(self, realm: Realm) -> None:
|
def __init__(self, realm: Realm) -> None:
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
|
@@ -10,9 +10,10 @@ from django.conf import settings
|
|||||||
from django.core.management.base import CommandError, CommandParser
|
from django.core.management.base import CommandError, CommandParser
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from zerver.lib.email_mirror import mirror_email_message
|
from zerver.lib.email_mirror import validate_to_address
|
||||||
from zerver.lib.email_mirror_helpers import encode_email_address, get_channel_email_token
|
from zerver.lib.email_mirror_helpers import encode_email_address, get_channel_email_token
|
||||||
from zerver.lib.management import ZulipBaseCommand
|
from zerver.lib.management import ZulipBaseCommand
|
||||||
|
from zerver.lib.queue import queue_json_publish_rollback_unsafe
|
||||||
from zerver.models import Realm, UserProfile
|
from zerver.models import Realm, UserProfile
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
@@ -99,9 +100,15 @@ Example:
|
|||||||
)
|
)
|
||||||
self._prepare_message(message, realm, stream, creator, sender)
|
self._prepare_message(message, realm, stream, creator, sender)
|
||||||
|
|
||||||
mirror_email_message(
|
rcpt_to = message["To"].addresses[0].addr_spec
|
||||||
message["To"].addresses[0].addr_spec,
|
validate_to_address(rcpt_to)
|
||||||
base64.b64encode(message.as_bytes()).decode(),
|
|
||||||
|
queue_json_publish_rollback_unsafe(
|
||||||
|
"email_mirror",
|
||||||
|
{
|
||||||
|
"rcpt_to": rcpt_to,
|
||||||
|
"msg_base64": base64.b64encode(message.as_bytes()).decode(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _does_fixture_path_exist(self, fixture_path: str) -> bool:
|
def _does_fixture_path_exist(self, fixture_path: str) -> bool:
|
||||||
|
@@ -3,14 +3,11 @@ import base64
|
|||||||
import email.parser
|
import email.parser
|
||||||
import email.policy
|
import email.policy
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
from collections.abc import Callable, Mapping
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from email.headerregistry import Address
|
from email.headerregistry import Address
|
||||||
from email.message import EmailMessage, MIMEPart
|
from email.message import EmailMessage, MIMEPart
|
||||||
from smtplib import SMTPException, SMTPSenderRefused
|
from smtplib import SMTPException, SMTPSenderRefused
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
@@ -52,17 +49,13 @@ from zerver.lib.email_notifications import convert_html_to_markdown
|
|||||||
from zerver.lib.send_email import FromAddress
|
from zerver.lib.send_email import FromAddress
|
||||||
from zerver.lib.streams import ensure_stream
|
from zerver.lib.streams import ensure_stream
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import mock_queue_publish, most_recent_message, most_recent_usermessage
|
from zerver.lib.test_helpers import most_recent_message, most_recent_usermessage
|
||||||
from zerver.models import Attachment, Recipient, Stream, UserProfile
|
from zerver.models import Attachment, Recipient, Stream, UserProfile
|
||||||
from zerver.models.groups import NamedUserGroup, SystemGroups
|
from zerver.models.groups import NamedUserGroup, SystemGroups
|
||||||
from zerver.models.messages import Message
|
from zerver.models.messages import Message
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
from zerver.models.streams import get_stream
|
from zerver.models.streams import get_stream
|
||||||
from zerver.models.users import get_system_bot
|
from zerver.models.users import get_system_bot
|
||||||
from zerver.worker.email_mirror import MirrorWorker
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
|
|
||||||
|
|
||||||
logger_name = "zerver.lib.email_mirror"
|
logger_name = "zerver.lib.email_mirror"
|
||||||
|
|
||||||
@@ -1725,153 +1718,6 @@ class TestReplyExtraction(ZulipTestCase):
|
|||||||
self.assertEqual(message.content, convert_html_to_markdown(html))
|
self.assertEqual(message.content, convert_html_to_markdown(html))
|
||||||
|
|
||||||
|
|
||||||
class TestScriptMTA(ZulipTestCase):
|
|
||||||
def test_success(self) -> None:
|
|
||||||
script = os.path.join(os.path.dirname(__file__), "../../scripts/lib/email-mirror-postfix")
|
|
||||||
|
|
||||||
user_profile = self.example_user("hamlet")
|
|
||||||
sender = self.example_email("hamlet")
|
|
||||||
stream = get_stream("Denmark", get_realm("zulip"))
|
|
||||||
email_token = get_channel_email_token(stream, creator=user_profile, sender=user_profile)
|
|
||||||
stream_to_address = encode_email_address(stream.name, email_token)
|
|
||||||
|
|
||||||
mail_template = self.fixture_data("simple.txt", type="email")
|
|
||||||
mail = mail_template.format(stream_to_address=stream_to_address, sender=sender)
|
|
||||||
subprocess.run(
|
|
||||||
[script, "-r", stream_to_address, "-s", settings.SHARED_SECRET, "-t"],
|
|
||||||
input=mail,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_error_no_recipient(self) -> None:
|
|
||||||
script = os.path.join(os.path.dirname(__file__), "../../scripts/lib/email-mirror-postfix")
|
|
||||||
|
|
||||||
user_profile = self.example_user("hamlet")
|
|
||||||
sender = self.example_email("hamlet")
|
|
||||||
stream = get_stream("Denmark", get_realm("zulip"))
|
|
||||||
email_token = get_channel_email_token(stream, creator=user_profile, sender=user_profile)
|
|
||||||
stream_to_address = encode_email_address(stream.name, email_token)
|
|
||||||
mail_template = self.fixture_data("simple.txt", type="email")
|
|
||||||
mail = mail_template.format(stream_to_address=stream_to_address, sender=sender)
|
|
||||||
p = subprocess.run(
|
|
||||||
[script, "-s", settings.SHARED_SECRET, "-t"],
|
|
||||||
input=mail,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
p.stdout,
|
|
||||||
"5.1.1 Bad destination mailbox address: No missed message email address.\n",
|
|
||||||
)
|
|
||||||
self.assertEqual(p.returncode, 67)
|
|
||||||
|
|
||||||
|
|
||||||
class TestEmailMirrorTornadoView(ZulipTestCase):
|
|
||||||
def send_private_message(self) -> str:
|
|
||||||
self.login("othello")
|
|
||||||
cordelia = self.example_user("cordelia")
|
|
||||||
iago = self.example_user("iago")
|
|
||||||
result = self.client_post(
|
|
||||||
"/json/messages",
|
|
||||||
{
|
|
||||||
"type": "private",
|
|
||||||
"content": "test_receive_missed_message_email_messages",
|
|
||||||
"to": orjson.dumps([cordelia.id, iago.id]).decode(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assert_json_success(result)
|
|
||||||
|
|
||||||
user_profile = self.example_user("cordelia")
|
|
||||||
user_message = most_recent_usermessage(user_profile)
|
|
||||||
return create_missed_message_address(user_profile, user_message.message)
|
|
||||||
|
|
||||||
def send_offline_message(self, to_address: str, sender: UserProfile) -> "TestHttpResponse":
|
|
||||||
mail_template = self.fixture_data("simple.txt", type="email")
|
|
||||||
mail = mail_template.format(stream_to_address=to_address, sender=sender.delivery_email)
|
|
||||||
msg_base64 = base64.b64encode(mail.encode()).decode()
|
|
||||||
|
|
||||||
def check_queue_json_publish(
|
|
||||||
queue_name: str,
|
|
||||||
event: Mapping[str, Any],
|
|
||||||
processor: Callable[[Any], None] | None = None,
|
|
||||||
) -> None:
|
|
||||||
self.assertEqual(queue_name, "email_mirror")
|
|
||||||
self.assertEqual(event, {"rcpt_to": to_address, "msg_base64": msg_base64})
|
|
||||||
MirrorWorker().consume(event)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
self.get_last_message().content,
|
|
||||||
"This is a plain-text message for testing Zulip.",
|
|
||||||
)
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
"rcpt_to": to_address,
|
|
||||||
"msg_base64": msg_base64,
|
|
||||||
"secret": settings.SHARED_SECRET,
|
|
||||||
}
|
|
||||||
|
|
||||||
with mock_queue_publish("zerver.lib.email_mirror.queue_json_publish_rollback_unsafe") as m:
|
|
||||||
m.side_effect = check_queue_json_publish
|
|
||||||
return self.client_post("/api/internal/email_mirror_message", post_data)
|
|
||||||
|
|
||||||
def test_success_stream(self) -> None:
|
|
||||||
stream = get_stream("Denmark", get_realm("zulip"))
|
|
||||||
user_profile = self.example_user("hamlet")
|
|
||||||
email_token = get_channel_email_token(stream, creator=user_profile, sender=user_profile)
|
|
||||||
stream_to_address = encode_email_address(stream.name, email_token)
|
|
||||||
result = self.send_offline_message(stream_to_address, self.example_user("hamlet"))
|
|
||||||
self.assert_json_success(result)
|
|
||||||
|
|
||||||
def test_error_to_stream_with_wrong_address(self) -> None:
|
|
||||||
stream = get_stream("Denmark", get_realm("zulip"))
|
|
||||||
user_profile = self.example_user("hamlet")
|
|
||||||
email_token = get_channel_email_token(stream, creator=user_profile, sender=user_profile)
|
|
||||||
stream_to_address = encode_email_address(stream.name, email_token)
|
|
||||||
# get the email_token:
|
|
||||||
token = decode_email_address(stream_to_address)[0]
|
|
||||||
stream_to_address = stream_to_address.replace(token, "Wrong_token")
|
|
||||||
|
|
||||||
result = self.send_offline_message(stream_to_address, self.example_user("hamlet"))
|
|
||||||
self.assert_json_error(
|
|
||||||
result,
|
|
||||||
"5.1.1 Bad destination mailbox address: "
|
|
||||||
"Bad stream token from email recipient " + stream_to_address,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_success_to_stream_with_good_token_wrong_stream_name(self) -> None:
|
|
||||||
stream = get_stream("Denmark", get_realm("zulip"))
|
|
||||||
user_profile = self.example_user("hamlet")
|
|
||||||
email_token = get_channel_email_token(stream, creator=user_profile, sender=user_profile)
|
|
||||||
stream_to_address = encode_email_address(stream.name, email_token)
|
|
||||||
stream_to_address = stream_to_address.replace("denmark", "Wrong_name")
|
|
||||||
|
|
||||||
result = self.send_offline_message(stream_to_address, self.example_user("hamlet"))
|
|
||||||
self.assert_json_success(result)
|
|
||||||
|
|
||||||
def test_success_to_private(self) -> None:
|
|
||||||
mm_address = self.send_private_message()
|
|
||||||
result = self.send_offline_message(mm_address, self.example_user("cordelia"))
|
|
||||||
self.assert_json_success(result)
|
|
||||||
|
|
||||||
def test_using_mm_address_multiple_times(self) -> None:
|
|
||||||
mm_address = self.send_private_message()
|
|
||||||
# there is no longer a usage limit. Ensure we can send multiple times.
|
|
||||||
for i in range(5):
|
|
||||||
result = self.send_offline_message(mm_address, self.example_user("cordelia"))
|
|
||||||
self.assert_json_success(result)
|
|
||||||
|
|
||||||
def test_wrong_missed_email_private_message(self) -> None:
|
|
||||||
self.send_private_message()
|
|
||||||
mm_address = "mm" + ("x" * 32) + "@testserver"
|
|
||||||
result = self.send_offline_message(mm_address, self.example_user("cordelia"))
|
|
||||||
self.assert_json_error(
|
|
||||||
result,
|
|
||||||
"5.1.1 Bad destination mailbox address: Zulip notification reply address is invalid.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestStreamEmailMessagesSubjectStripping(ZulipTestCase):
|
class TestStreamEmailMessagesSubjectStripping(ZulipTestCase):
|
||||||
def test_process_message_strips_subject(self) -> None:
|
def test_process_message_strips_subject(self) -> None:
|
||||||
user_profile = self.example_user("hamlet")
|
user_profile = self.example_user("hamlet")
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
|
|
||||||
from zerver.decorator import internal_api_view
|
|
||||||
from zerver.lib.email_mirror import mirror_email_message
|
|
||||||
from zerver.lib.exceptions import JsonableError
|
|
||||||
from zerver.lib.response import json_success
|
|
||||||
from zerver.lib.typed_endpoint import typed_endpoint
|
|
||||||
|
|
||||||
|
|
||||||
@internal_api_view(False)
|
|
||||||
@typed_endpoint
|
|
||||||
def email_mirror_message(
|
|
||||||
request: HttpRequest,
|
|
||||||
*,
|
|
||||||
rcpt_to: str,
|
|
||||||
msg_base64: str,
|
|
||||||
) -> HttpResponse:
|
|
||||||
result = mirror_email_message(rcpt_to, msg_base64)
|
|
||||||
if result["status"] == "error":
|
|
||||||
raise JsonableError(result["msg"])
|
|
||||||
return json_success(request)
|
|
@@ -58,7 +58,6 @@ from zerver.views.custom_profile_fields import (
|
|||||||
from zerver.views.digest import digest_page
|
from zerver.views.digest import digest_page
|
||||||
from zerver.views.documentation import IntegrationView, MarkdownDirectoryView, integration_doc
|
from zerver.views.documentation import IntegrationView, MarkdownDirectoryView, integration_doc
|
||||||
from zerver.views.drafts import create_drafts, delete_draft, edit_draft, fetch_drafts
|
from zerver.views.drafts import create_drafts, delete_draft, edit_draft, fetch_drafts
|
||||||
from zerver.views.email_mirror import email_mirror_message
|
|
||||||
from zerver.views.events_register import events_register_backend
|
from zerver.views.events_register import events_register_backend
|
||||||
from zerver.views.health import health
|
from zerver.views.health import health
|
||||||
from zerver.views.home import accounts_accept_terms, desktop_home, doc_permalinks_view, home
|
from zerver.views.home import accounts_accept_terms, desktop_home, doc_permalinks_view, home
|
||||||
@@ -808,7 +807,6 @@ for app_name in settings.EXTRA_INSTALLED_APPS:
|
|||||||
# Used internally for communication between command-line, tusd, Django,
|
# Used internally for communication between command-line, tusd, Django,
|
||||||
# and Tornado processes
|
# and Tornado processes
|
||||||
urls += [
|
urls += [
|
||||||
path("api/internal/email_mirror_message", email_mirror_message),
|
|
||||||
path("api/internal/notify_tornado", notify),
|
path("api/internal/notify_tornado", notify),
|
||||||
path("api/internal/tusd", handle_tusd_hook),
|
path("api/internal/tusd", handle_tusd_hook),
|
||||||
path("api/internal/web_reload_clients", web_reload_clients),
|
path("api/internal/web_reload_clients", web_reload_clients),
|
||||||
|
Reference in New Issue
Block a user