Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
87 KB
Referenced Files
None
Subscribers
None
diff --git a/docker/postfix/rootfs/etc/postfix/main.cf b/docker/postfix/rootfs/etc/postfix/main.cf
index 09d4fb85..024f4782 100644
--- a/docker/postfix/rootfs/etc/postfix/main.cf
+++ b/docker/postfix/rootfs/etc/postfix/main.cf
@@ -1,612 +1,604 @@
compatibility_level = 2
# SOFT BOUNCE
#
# The soft_bounce parameter provides a limited safety net for
# testing. When soft_bounce is enabled, mail will remain queued that
# would otherwise bounce. This parameter disables locally-generated
# bounces, and prevents the SMTP server from rejecting mail permanently
# (by changing 5xx replies into 4xx replies). However, soft_bounce
# is no cure for address rewriting mistakes or mail routing mistakes.
#
#soft_bounce = no
# LOCAL PATHNAME INFORMATION
queue_directory = /var/spool/postfix
command_directory = /usr/sbin
daemon_directory = /usr/libexec/postfix
data_directory = /var/lib/postfix
# QUEUE AND PROCESS OWNERSHIP
#
# The mail_owner parameter specifies the owner of the Postfix queue
# and of most Postfix daemon processes. Specify the name of a user
# account THAT DOES NOT SHARE ITS USER OR GROUP ID WITH OTHER ACCOUNTS
# AND THAT OWNS NO OTHER FILES OR PROCESSES ON THE SYSTEM. In
# particular, don't specify nobody or daemon. PLEASE USE A DEDICATED
# USER.
#
mail_owner = postfix
# The default_privs parameter specifies the default rights used by
# the local delivery agent for delivery to external file or command.
# These rights are used in the absence of a recipient user context.
# DO NOT SPECIFY A PRIVILEGED USER OR THE POSTFIX OWNER.
#
#default_privs = nobody
# DOMAINS
myhostname = postfix.APP_DOMAIN
mydomain = APP_DOMAIN
myorigin = $mydomain
# RECEIVING MAIL
# The inet_interfaces parameter specifies the network interface
# addresses that this mail system receives mail on. By default,
# the software claims all active interfaces on the machine. The
# parameter also controls delivery of mail to user@[ip.address].
#
# See also the proxy_interfaces parameter, for network addresses that
# are forwarded to us via a proxy or network address translator.
#
# Note: you need to stop/start Postfix when this parameter changes.
#
#inet_interfaces = all
#inet_interfaces = $myhostname
#inet_interfaces = $myhostname, localhost
inet_interfaces = all
# Enable IPv4, and IPv6 if supported
inet_protocols = all
# The proxy_interfaces parameter specifies the network interface
# addresses that this mail system receives mail on by way of a
# proxy or network address translation unit. This setting extends
# the address list specified with the inet_interfaces parameter.
#
# You must specify your proxy/NAT addresses when your system is a
# backup MX host for other domains, otherwise mail delivery loops
# will happen when the primary MX host is down.
#
#proxy_interfaces =
#proxy_interfaces = 1.2.3.4
# Specify which domains should be delivered locally
mydestination = $myhostname $mydomain mysql:/etc/postfix/sql/mydestination.cf
# Required to correctly resolve the imap host in the container
lmtp_host_lookup = native
# lmtp for local transport
local_transport = lmtp:LMTP_DESTINATION
# The unknown_local_recipient_reject_code specifies the SMTP server
# response code when a recipient domain matches $mydestination or
# ${proxy,inet}_interfaces, while $local_recipient_maps is non-empty
# and the recipient address or address local-part is not found.
#
# The default setting is 550 (reject mail) but it is safer to start
# with 450 (try again later) until you are certain that your
# local_recipient_maps settings are OK.
#
unknown_local_recipient_reject_code = 550
# TRUST AND RELAY CONTROL
# The mynetworks parameter specifies the list of "trusted" SMTP
# clients that have more privileges than "strangers".
#
# In particular, "trusted" SMTP clients are allowed to relay mail
# through Postfix. See the smtpd_recipient_restrictions parameter
# in postconf(5).
#
# You can specify the list of "trusted" network addresses by hand
# or you can let Postfix do it for you (which is the default).
#
# By default (mynetworks_style = subnet), Postfix "trusts" SMTP
# clients in the same IP subnetworks as the local machine.
# On Linux, this works correctly only with interfaces specified
# with the "ifconfig" command.
#
# Specify "mynetworks_style = class" when Postfix should "trust" SMTP
# clients in the same IP class A/B/C networks as the local machine.
# Don't do this with a dialup site - it would cause Postfix to "trust"
# your entire provider's network. Instead, specify an explicit
# mynetworks list by hand, as described below.
#
# Specify "mynetworks_style = host" when Postfix should "trust"
# only the local machine.
#
#mynetworks_style = class
#mynetworks_style = subnet
#mynetworks_style = host
# Alternatively, you can specify the mynetworks list by hand, in
# which case Postfix ignores the mynetworks_style setting.
#
# Specify an explicit list of network/netmask patterns, where the
# mask specifies the number of bits in the network part of a host
# address.
#
# You can also specify the absolute pathname of a pattern file instead
# of listing the patterns here. Specify type:table for table-based lookups
# (the value on the table right-hand side is not used).
#
# Trust the docker network
mynetworks = MYNETWORKS
#mynetworks = 168.100.189.0/28, 127.0.0.0/8
#mynetworks = $config_directory/mynetworks
#mynetworks = hash:/etc/postfix/network_table
# The relay_domains parameter restricts what destinations this system will
# relay mail to. See the smtpd_recipient_restrictions description in
# postconf(5) for detailed information.
#
# By default, Postfix relays mail
# - from "trusted" clients (IP address matches $mynetworks) to any destination,
# - from "untrusted" clients to destinations that match $relay_domains or
# subdomains thereof, except addresses with sender-specified routing.
# The default relay_domains value is $mydestination.
#
# In addition to the above, the Postfix SMTP server by default accepts mail
# that Postfix is final destination for:
# - destinations that match $inet_interfaces or $proxy_interfaces,
# - destinations that match $mydestination
# - destinations that match $virtual_alias_domains,
# - destinations that match $virtual_mailbox_domains.
# These destinations do not need to be listed in $relay_domains.
#
# Specify a list of hosts or domains, /file/name patterns or type:name
# lookup tables, separated by commas and/or whitespace. Continue
# long lines by starting the next line with whitespace. A file name
# is replaced by its contents; a type:name table is matched when a
# (parent) domain appears as lookup key.
#
# NOTE: Postfix will not automatically forward mail for domains that
# list this system as their primary or backup MX host. See the
# permit_mx_backup restriction description in postconf(5).
#
#relay_domains = $mydestination
# INTERNET OR INTRANET
# The relayhost parameter specifies the default host to send mail to
# when no entry is matched in the optional transport(5) table. When
# no relayhost is given, mail is routed directly to the destination.
#
# On an intranet, specify the organizational domain name. If your
# internal DNS uses no MX records, specify the name of the intranet
# gateway host instead.
#
# In the case of SMTP, specify a domain, host, host:port, [host]:port,
# [address] or [address]:port; the form [host] turns off MX lookups.
#
# If you're connected via UUCP, see also the default_transport parameter.
#
#relayhost = $mydomain
#relayhost = [gateway.my.domain]
#relayhost = [mailserver.isp.tld]
#relayhost = uucphost
#relayhost = [an.ip.add.ress]
# REJECTING UNKNOWN RELAY USERS
#
# The relay_recipient_maps parameter specifies optional lookup tables
# with all addresses in the domains that match $relay_domains.
#
# If this parameter is defined, then the SMTP server will reject
# mail for unknown relay users. This feature is off by default.
#
# The right-hand side of the lookup tables is conveniently ignored.
# In the left-hand side, specify an @domain.tld wild-card, or specify
# a user@domain.tld address.
#
#relay_recipient_maps = hash:/etc/postfix/relay_recipients
# INPUT RATE CONTROL
#
# The in_flow_delay configuration parameter implements mail input
# flow control. This feature is turned on by default, although it
# still needs further development (it's disabled on SCO UNIX due
# to an SCO bug).
#
# A Postfix process will pause for $in_flow_delay seconds before
# accepting a new message, when the message arrival rate exceeds the
# message delivery rate. With the default 100 SMTP server process
# limit, this limits the mail inflow to 100 messages a second more
# than the number of messages delivered per second.
#
# Specify 0 to disable the feature. Valid delays are 0..10.
#
#in_flow_delay = 1s
# ADDRESS REWRITING
#
# The ADDRESS_REWRITING_README document gives information about
# address masquerading or other forms of address rewriting including
# username->Firstname.Lastname mapping.
# ADDRESS REDIRECTION (VIRTUAL DOMAIN)
#
# The VIRTUAL_README document gives information about the many forms
# of domain hosting that Postfix supports.
# "USER HAS MOVED" BOUNCE MESSAGES
#
# See the discussion in the ADDRESS_REWRITING_README document.
# TRANSPORT MAP
#
# See the discussion in the ADDRESS_REWRITING_README document.
# ALIAS DATABASE
#
# The alias_maps parameter specifies the list of alias databases used
# by the local delivery agent. The default list is system dependent.
#
# On systems with NIS, the default is to search the local alias
# database, then the NIS alias database. See aliases(5) for syntax
# details.
#
# If you change the alias database, run "postalias /etc/aliases" (or
# wherever your system stores the mail alias file), or simply run
# "newaliases" to build the necessary DBM or DB file.
#
# It will take a minute or so before changes become visible. Use
# "postfix reload" to eliminate the delay.
#
#alias_maps = dbm:/etc/aliases
alias_maps = hash:/etc/aliases
#alias_maps = hash:/etc/aliases, nis:mail.aliases
#alias_maps = netinfo:/aliases
# The alias_database parameter specifies the alias database(s) that
# are built with "newaliases" or "sendmail -bi". This is a separate
# configuration parameter, because alias_maps (see above) may specify
# tables that are not necessarily all under control by Postfix.
#
#alias_database = dbm:/etc/aliases
#alias_database = dbm:/etc/mail/aliases
alias_database = hash:/etc/aliases
#alias_database = hash:/etc/aliases, hash:/opt/majordomo/aliases
# ADDRESS EXTENSIONS (e.g., user+foo)
#
# The recipient_delimiter parameter specifies the separator between
# user names and address extensions (user+foo). See canonical(5),
# local(8), relocated(5) and virtual(5) for the effects this has on
# aliases, canonical, virtual, relocated and .forward file lookups.
# Basically, the software tries user+foo and .forward+foo before
# trying user and .forward.
#
#recipient_delimiter = +
# DELIVERY TO MAILBOX
#
# The home_mailbox parameter specifies the optional pathname of a
# mailbox file relative to a user's home directory. The default
# mailbox file is /var/spool/mail/user or /var/mail/user. Specify
# "Maildir/" for qmail-style delivery (the / is required).
#
#home_mailbox = Mailbox
#home_mailbox = Maildir/
# The mail_spool_directory parameter specifies the directory where
# UNIX-style mailboxes are kept. The default setting depends on the
# system type.
#
#mail_spool_directory = /var/mail
#mail_spool_directory = /var/spool/mail
# The mailbox_command parameter specifies the optional external
# command to use instead of mailbox delivery. The command is run as
# the recipient with proper HOME, SHELL and LOGNAME environment settings.
# Exception: delivery for root is done as $default_user.
#
# Other environment variables of interest: USER (recipient username),
# EXTENSION (address extension), DOMAIN (domain part of address),
# and LOCAL (the address localpart).
#
# Unlike other Postfix configuration parameters, the mailbox_command
# parameter is not subjected to $parameter substitutions. This is to
# make it easier to specify shell syntax (see example below).
#
# Avoid shell meta characters because they will force Postfix to run
# an expensive shell process. Procmail alone is expensive enough.
#
# IF YOU USE THIS TO DELIVER MAIL SYSTEM-WIDE, YOU MUST SET UP AN
# ALIAS THAT FORWARDS MAIL FOR ROOT TO A REAL USER.
#
#mailbox_command = /some/where/procmail
#mailbox_command = /some/where/procmail -a "$EXTENSION"
# If using the cyrus-imapd IMAP server deliver local mail to the IMAP
# server using LMTP (Local Mail Transport Protocol), this is prefered
# over the older cyrus deliver program by setting the
# mailbox_transport as below:
#
# mailbox_transport = lmtp:unix:/var/lib/imap/socket/lmtp
#
# The efficiency of LMTP delivery for cyrus-imapd can be enhanced via
# these settings.
#
# local_destination_recipient_limit = 300
# local_destination_concurrency_limit = 5
#
# Of course you should adjust these settings as appropriate for the
# capacity of the hardware you are using. The recipient limit setting
# can be used to take advantage of the single instance message store
# capability of Cyrus. The concurrency limit can be used to control
# how many simultaneous LMTP sessions will be permitted to the Cyrus
# message store.
#
# Cyrus IMAP via command line. Uncomment the "cyrus...pipe" and
# subsequent line in master.cf.
#mailbox_transport = cyrus
# The fallback_transport specifies the optional transport in master.cf
# to use for recipients that are not found in the UNIX passwd database.
# This parameter has precedence over the luser_relay parameter.
#
# Specify a string of the form transport:nexthop, where transport is
# the name of a mail delivery transport defined in master.cf. The
# :nexthop part is optional. For more details see the sample transport
# configuration file.
#
# NOTE: if you use this feature for accounts not in the UNIX password
# file, then you must update the "local_recipient_maps" setting in
# the main.cf file, otherwise the SMTP server will reject mail for
# non-UNIX accounts with "User unknown in local recipient table".
#
#fallback_transport = lmtp:unix:/var/lib/imap/socket/lmtp
#fallback_transport =
# The luser_relay parameter specifies an optional destination address
# for unknown recipients. By default, mail for unknown@$mydestination,
# unknown@[$inet_interfaces] or unknown@[$proxy_interfaces] is returned
# as undeliverable.
#
# The following expansions are done on luser_relay: $user (recipient
# username), $shell (recipient shell), $home (recipient home directory),
# $recipient (full recipient address), $extension (recipient address
# extension), $domain (recipient domain), $local (entire recipient
# localpart), $recipient_delimiter. Specify ${name?value} or
# ${name:value} to expand value only when $name does (does not) exist.
#
# luser_relay works only for the default Postfix local delivery agent.
#
# NOTE: if you use this feature for accounts not in the UNIX password
# file, then you must specify "local_recipient_maps =" (i.e. empty) in
# the main.cf file, otherwise the SMTP server will reject mail for
# non-UNIX accounts with "User unknown in local recipient table".
#
#luser_relay = $user@other.host
#luser_relay = $local@other.host
#luser_relay = admin+$local
# JUNK MAIL CONTROLS
#
# The controls listed here are only a very small subset. The file
# SMTPD_ACCESS_README provides an overview.
# The header_checks parameter specifies an optional table with patterns
# that each logical message header is matched against, including
# headers that span multiple physical lines.
#
# By default, these patterns also apply to MIME headers and to the
# headers of attached messages. With older Postfix versions, MIME and
# attached message headers were treated as body text.
#
# For details, see "man header_checks".
#
#header_checks = regexp:/etc/postfix/header_checks
# FAST ETRN SERVICE
#
# Postfix maintains per-destination logfiles with information about
# deferred mail, so that mail can be flushed quickly with the SMTP
# "ETRN domain.tld" command, or by executing "sendmail -qRdomain.tld".
# See the ETRN_README document for a detailed description.
#
# The fast_flush_domains parameter controls what destinations are
# eligible for this service. By default, they are all domains that
# this server is willing to relay mail to.
#
#fast_flush_domains = $relay_domains
# SHOW SOFTWARE VERSION OR NOT
#
# The smtpd_banner parameter specifies the text that follows the 220
# code in the SMTP server's greeting banner. Some people like to see
# the mail version advertised. By default, Postfix shows no version.
#
# You MUST specify $myhostname at the start of the text. That is an
# RFC requirement. Postfix itself does not care.
#
#smtpd_banner = $myhostname ESMTP $mail_name
#smtpd_banner = $myhostname ESMTP $mail_name ($mail_version)
# PARALLEL DELIVERY TO THE SAME DESTINATION
#
# How many parallel deliveries to the same user or domain? With local
# delivery, it does not make sense to do massively parallel delivery
# to the same user, because mailbox updates must happen sequentially,
# and expensive pipelines in .forward files can cause disasters when
# too many are run at the same time. With SMTP deliveries, 10
# simultaneous connections to the same domain could be sufficient to
# raise eyebrows.
#
# Each message delivery transport has its XXX_destination_concurrency_limit
# parameter. The default is $default_destination_concurrency_limit for
# most delivery transports. For the local delivery agent the default is 2.
#local_destination_concurrency_limit = 2
#default_destination_concurrency_limit = 20
# DEBUGGING CONTROL
debug_peer_level = 2
debugger_command =
PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
ddd $daemon_directory/$process_name $process_id & sleep 5
# INSTALL-TIME CONFIGURATION INFORMATION
sendmail_path = /usr/sbin/sendmail.postfix
newaliases_path = /usr/bin/newaliases.postfix
mailq_path = /usr/bin/mailq.postfix
setgid_group = postdrop
html_directory = no
manpage_directory = /usr/share/man
sample_directory = /usr/share/doc/postfix/samples
readme_directory = /usr/share/doc/postfix/README_FILES
# TLS CONFIGURATION
#
# Basic Postfix TLS configuration by default with self-signed certificate
# for inbound SMTP and also opportunistic TLS for outbound SMTP.
# The full pathname of a file with the Postfix SMTP server RSA certificate
# in PEM format. Intermediate certificates should be included in general,
# the server certificate first, then the issuing CA(s) (bottom-up order).
#
smtpd_tls_cert_file = /etc/pki/tls/private/postfix.pem
# The full pathname of a file with the Postfix SMTP server RSA private key
# in PEM format. The private key must be accessible without a pass-phrase,
# i.e. it must not be encrypted.
#
smtpd_tls_key_file = /etc/pki/tls/private/postfix.pem
# Announce STARTTLS support to remote SMTP clients, but do not require that
# clients use TLS encryption (opportunistic TLS inbound).
#
smtpd_tls_security_level = may
# Directory with PEM format Certification Authority certificates that the
# Postfix SMTP client uses to verify a remote SMTP server certificate.
#
smtp_tls_CApath = /etc/pki/tls/certs
# The full pathname of a file containing CA certificates of root CAs
# trusted to sign either remote SMTP server certificates or intermediate CA
# certificates.
#
smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt
# Use TLS if this is supported by the remote SMTP server, otherwise use
# plaintext (opportunistic TLS outbound).
#
smtp_tls_security_level = may
+
meta_directory = /etc/postfix
shlib_directory = /usr/lib64/postfix
recipient_delimiter = +
-transport_maps = regexp:/etc/postfix/transport
smtpd_tls_auth_only = no
+smtpd_helo_required = yes
+smtpd_peername_lookup = yes
+smtpd_sasl_auth_enable = yes
+maillog_file = /dev/stdout
+message_size_limit = MESSAGE_SIZE_LIMIT
+
+# Disable BDAT support without the useless "discarding EHLO keywords: CHUNKING" message
+smtpd_discard_ehlo_keywords = chunking, silent-discard
+
+content_filter = smtp-amavis:[AMAVIS_HOST]:13024
+transport_maps = regexp:/etc/postfix/transport
+
+smtpd_sender_login_maps = mysql:/etc/postfix/sql/local_recipient_maps.cf
local_recipient_maps =
mysql:/etc/postfix/sql/local_recipient_maps.cf,
mysql:/etc/postfix/sql/local_recipient_maps_groups.cf,
mysql:/etc/postfix/sql/local_recipient_maps_shared_folders.cf
virtual_alias_maps =
$alias_maps,
mysql:/etc/postfix/sql/virtual_alias_maps.cf,
mysql:/etc/postfix/sql/virtual_alias_maps_groups.cf,
mysql:/etc/postfix/sql/virtual_alias_maps_shared_folders.cf
-# Inbound
+# Inbound (restrictions in order of execution)
smtpd_client_restrictions =
permit_mynetworks,
reject_unknown_reverse_client_hostname,
#reject_rbl_client zen.spamhaus.org,
#reject_rhsbl_reverse_client dbl.spamhaus.org
-smtpd_data_restrictions =
- reject_unauth_pipelining
-smtpd_helo_required = yes
smtpd_helo_restrictions =
permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
# reject_rhsbl_helo dbl.spamhaus.org
-# Local clients and authenticated clients may specify any destination domain
-smtpd_relay_restrictions =
- permit_mynetworks,
- permit_sasl_authenticated,
- reject_unauth_destination
-smtpd_recipient_restrictions =
- permit_mynetworks,
- reject_invalid_hostname,
- reject_non_fqdn_sender,
- reject_non_fqdn_recipient,
- reject_unauth_destination,
- #reject_rhsbl_recipient dbl.spamhaus.org,
- # TODO block suspended recipients (by domain or account)
- #check_recipient_access ldap:/etc/postfix/ldap/domain_suspended.cf,
- #check_recipient_access ldap:/etc/postfix/ldap/account_suspended.cf,
- check_policy_service unix:private/policy_greylist,
- permit
-smtpd_peername_lookup = yes
-smtpd_sasl_auth_enable = yes
-
-smtpd_sender_login_maps =
- mysql:/etc/postfix/sql/local_recipient_maps.cf
-
smtpd_sender_restrictions =
# We used to also block spammers via sender_access
check_sender_access hash:/etc/postfix/sender_access,
permit_mynetworks,
reject_unknown_sender_domain,
#check_client_access hash:/etc/postfix/client_access,
#check_client_access cidr:/etc/postfix/client_access_cidr,
reject_unlisted_sender,
# Uses smtpd_sender_login_maps
reject_unauthenticated_sender_login_mismatch,
reject_unauth_destination,
check_policy_service unix:private/policy_spf,
#reject_rhsbl_sender dbl.spamhaus.org,
permit
-
+# Local clients and authenticated clients may specify any destination domain
+smtpd_relay_restrictions =
+ permit_mynetworks,
+ permit_sasl_authenticated,
+ reject_unauth_destination
+smtpd_recipient_restrictions =
+ permit_mynetworks,
+ reject_invalid_hostname,
+ reject_non_fqdn_sender,
+ reject_non_fqdn_recipient,
+ reject_unauth_destination,
+ #reject_rhsbl_recipient dbl.spamhaus.org,
+ check_policy_service unix:private/policy_reception,
+ permit
+smtpd_data_restrictions = reject_unauth_pipelining
# Outbound
submission_data_restrictions =
# Final hook for rate-limit request and decision
# to allow/deny sender access.
# We use this in place of reject_sender_login_mismatch to support e.g. delegation.
check_policy_service unix:private/policy_submission
submission_client_restrictions =
#reject_unknown_reverse_client_hostname,
#reject_rbl_client zen.spamhaus.org,
#reject_rhsbl_reverse_client dbl.spamhaus.org
submission_sender_restrictions =
reject_non_fqdn_sender,
# We used to also block spammers via sender_access
check_sender_access hash:/etc/postfix/sender_access,
permit_sasl_authenticated,
reject
submission_helo_restrictions =
#permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
#reject_rhsbl_helo dbl.spamhaus.org
submission_relay_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
submission_recipient_restrictions =
# FIXME These were lists of spammers to block by recipient, recipient mx and recipient domain
# check_recipient_access hash:/etc/postfix/recipient_access,
# check_recipient_mx_access hash:/etc/postfix/recipient_mx_access,
# check_recipient_ns_access hash:/etc/postfix/recipient_ns_access,
# Hook to collect recipients
check_policy_service unix:private/policy_submission,
permit_sasl_authenticated,
# permit_mynetworks,
reject
-
-# Disable BDAT support without the useless "discarding EHLO keywords: CHUNKING" message
-smtpd_discard_ehlo_keywords = chunking, silent-discard
-
-content_filter = smtp-amavis:[AMAVIS_HOST]:13024
-
-maillog_file = /dev/stdout
-
-message_size_limit = MESSAGE_SIZE_LIMIT
diff --git a/docker/postfix/rootfs/etc/postfix/master.cf b/docker/postfix/rootfs/etc/postfix/master.cf
index 5718e628..a6eaeba7 100644
--- a/docker/postfix/rootfs/etc/postfix/master.cf
+++ b/docker/postfix/rootfs/etc/postfix/master.cf
@@ -1,141 +1,145 @@
# 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
# (yes) (yes) (yes) (never) (100) + args
# ==============================================================================
postlog unix-dgram n - n - 1 postlogd
# Inbound, port 25, no tls
10025 inet n - n - - smtpd
-o smtpd_upstream_proxy_protocol=
-o content_filter=smtp-amavis:[AMAVIS_HOST]:13024
-o cleanup_service_name=cleanup_inbound
# Internal Submission, no tls, no starttls
10587 inet n - - - - smtpd
-o syslog_name=postfix/submission-int
-o cleanup_service_name=cleanup_submission
-o content_filter=smtp-amavis:[AMAVIS_HOST]:13026
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_data_restrictions=$submission_data_restrictions
-o smtpd_client_restrictions=$submission_client_restrictions
-o smtpd_sender_restrictions=$submission_sender_restrictions
-o smtpd_recipient_restrictions=$submission_recipient_restrictions
-o smtpd_relay_restrictions=$submission_relay_restrictions
-o smtpd_helo_restrictions=$submission_helo_restrictions
-o smtpd_helo_required=yes
-o smtpd_peername_lookup=no
# External submission, starttls
0.0.0.0:11587 inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_upstream_proxy_protocol=
-o cleanup_service_name=cleanup_submission
-o content_filter=smtp-amavis:[AMAVIS_HOST]:13026
#-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_authenticated_header=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_data_restrictions=$submission_data_restrictions
-o smtpd_sender_restrictions=$submission_sender_restrictions
-o smtpd_recipient_restrictions=$submission_recipient_restrictions
-o smtpd_relay_restrictions=$submission_relay_restrictions
# External submission, ssl
0.0.0.0:11465 inet n - n - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_upstream_proxy_protocol=
-o cleanup_service_name=cleanup_submission
-o rewrite_service_name=rewrite_submission
-o content_filter=smtp-amavis:[AMAVIS_HOST]:13026
-o mydestination=
-o local_recipient_maps=
-o relay_domains=
-o relay_recipient_maps=
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_authenticated_header=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_sender_restrictions=$submission_sender_restrictions
-o smtpd_recipient_restrictions=$submission_recipient_restrictions
-o smtpd_relay_restrictions=$submission_relay_restrictions
-o smtpd_data_restrictions=$submission_data_restrictions
pickup fifo n - n 60 1 pickup
# This avoids that we have an endless loop after our script content filter
-o content_filter=
cleanup unix n - n - 0 cleanup
cleanup_inbound unix n - n - 0 cleanup
-o header_checks=regexp:/etc/postfix/header_checks.inbound
-o mime_header_checks=regexp:/etc/postfix/header_checks.inbound
cleanup_submission unix n - n - 0 cleanup
-o header_checks=regexp:/etc/postfix/header_checks.submission
-o mime_header_checks=regexp:/etc/postfix/header_checks.submission
cleanup_internal unix n - n - 0 cleanup
-o header_checks=regexp:/etc/postfix/header_checks.internal
-o mime_header_checks=regexp:/etc/postfix/header_checks.internal
qmgr fifo n - n 300 1 qmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - n - - smtp
relay unix - - n - - smtp
showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
# Filter email through Amavisd
smtp-amavis unix - - n - 3 smtp
-o smtp_data_done_timeout=1800
-o disable_dns_lookups=yes
-o smtp_send_xforward_command=yes
-o max_use=20
# Listener to re-inject email from Amavisd into Postfix
0.0.0.0:13025 inet n - n - 100 smtpd
-o syslog_name=postfix/amavis
-o cleanup_service_name=cleanup_internal
-o local_recipient_maps=
-o relay_recipient_maps=
-o content_filter=policy_mailfilter:dummy
-o smtpd_restriction_classes=
-o smtpd_client_restrictions=
-o smtpd_helo_restrictions=
-o smtpd_sender_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o mynetworks=MYNETWORKS
-o smtpd_authorized_xforward_hosts=MYNETWORKS
# Outbound
policy_submission unix - n n - - spawn
user=nobody argv=/usr/libexec/postfix/kolab_policy_submission
# Inbound
-policy_greylist unix - n n - - spawn
- user=nobody argv=/usr/libexec/postfix/kolab_policy greylist /api/webhooks/policy/greylist
+policy_reception unix - n n - - spawn
+ user=nobody argv=/usr/libexec/postfix/kolab_policy reception /api/webhooks/policy/reception
+
+# Inbound
+#policy_greylist unix - n n - - spawn
+# user=nobody argv=/usr/libexec/postfix/kolab_policy greylist /api/webhooks/policy/greylist
# Inbound
policy_spf unix - n n - - spawn
user=nobody argv=/usr/libexec/postfix/kolab_policy spf /api/webhooks/policy/spf
# Mailfilter via commandline, to be reinjected via sendmail.
policy_mailfilter unix - n n - 10 pipe
flags=Rq user=nobody null_sender=
argv=/usr/libexec/postfix/kolab_contentfilter_cli -f ${sender} -- ${recipient}
diff --git a/docker/postfix/rootfs/init.sh b/docker/postfix/rootfs/init.sh
index cad91d18..21cec917 100755
--- a/docker/postfix/rootfs/init.sh
+++ b/docker/postfix/rootfs/init.sh
@@ -1,88 +1,89 @@
#!/bin/bash
set -e
if [[ -f ${SSL_CERTIFICATE} ]]; then
cat ${SSL_CERTIFICATE} ${SSL_CERTIFICATE_FULLCHAIN} ${SSL_CERTIFICATE_KEY} > /etc/pki/tls/private/postfix.pem
chown postfix:mail /etc/pki/tls/private/postfix.pem
chmod 655 /etc/pki/tls/private/postfix.pem
fi
sed -i -r \
-e "s|LMTP_DESTINATION|${LMTP_DESTINATION:?"env required"}|g" \
-e "s|APP_DOMAIN|${APP_DOMAIN:?"env required"}|g" \
-e "s|MYNETWORKS|${MYNETWORKS:?"env required"}|g" \
-e "s|AMAVIS_HOST|${AMAVIS_HOST:?"env required"}|g" \
-e "s|MESSAGE_SIZE_LIMIT|${MESSAGE_SIZE_LIMIT:?"env required"}|g" \
/etc/postfix/main.cf
mkdir /var/log/kolab
+touch /var/log/kolab/postfix-content-filter.log
+#touch /var/log/kolab/postfix-policy-greylist.log
touch /var/log/kolab/postfix-policy-submission.log
+touch /var/log/kolab/postfix-policy-reception.log
touch /var/log/kolab/postfix-policy-spf.log
-touch /var/log/kolab/postfix-policy-greylist.log
-touch /var/log/kolab/postfix-content-filter.log
chmod -R 777 /var/log/kolab
chown -R postfix:mail /var/lib/postfix
chown -R postfix:mail /var/spool/postfix
/usr/sbin/postfix set-permissions
sed -i -r \
-e "s|APP_SERVICES_DOMAIN|$APP_SERVICES_DOMAIN|g" \
-e "s|SERVICES_PORT|$SERVICES_PORT|g" \
/etc/saslauthd.conf
/usr/sbin/saslauthd -m /run/saslauthd -a httpform -d &
# If host mounting /var/spool/postfix, we need to delete old pid file before
# starting services
rm -f /var/spool/postfix/pid/master.pid
/usr/libexec/postfix/aliasesdb
/usr/libexec/postfix/chroot-update
sed -i -r \
-e "s|MYNETWORKS|${MYNETWORKS:?"env requried"}|g" \
-e "s|AMAVIS_HOST|${AMAVIS_HOST:?"env requried"}|g" \
/etc/postfix/master.cf
if [ "$WITH_CONTENTFILTER" != "true" ]; then
echo "Disabling kolab content filter"
sed -i -r \
-e "s|content_filter=policy_mailfilter:dummy|content_filter=|g" \
/etc/postfix/master.cf
fi
if [ "$WITH_PROXY_PROTOCOL" == "true" ]; then
sed -i -r \
-e "s|smtpd_upstream_proxy_protocol=|smtpd_upstream_proxy_protocol=haproxy|g" \
/etc/postfix/master.cf
fi
if [ "$BLOCK_OUTGOING_EMAILS" == "true" ]; then
echo "default_transport = error:No outside emails." >> /etc/postfix/main.cf
fi
sed -i -r \
-e "s|SERVICES_HOST|http://$APP_SERVICES_DOMAIN:$SERVICES_PORT|g" \
/usr/libexec/postfix/kolab_policy*
sed -i -r \
-e "s|SERVICES_HOST|http://$APP_SERVICES_DOMAIN:$SERVICES_PORT|g" \
/usr/libexec/postfix/kolab_contentfilter*
sed -i -r \
-e "s|DB_HOST|${DB_HOST:?"env required"}|g" \
-e "s|DB_USERNAME|${DB_USERNAME:?"env required"}|g" \
-e "s|DB_PASSWORD|${DB_PASSWORD:?"env required"}|g" \
-e "s|DB_DATABASE|${DB_DATABASE:?"env required"}|g" \
/etc/postfix/sql/*
# echo "/$APP_DOMAIN/ lmtp:$LMTP_DESTINATION" >> /etc/postfix/transport
# postmap /etc/postfix/transport
postmap /etc/postfix/sender_access
/usr/sbin/postfix check
exec /usr/sbin/postfix -c /etc/postfix start-fg
diff --git a/src/app/Http/Controllers/API/V4/PolicyController.php b/src/app/Http/Controllers/API/V4/PolicyController.php
index dfa924ec..ca5725c5 100644
--- a/src/app/Http/Controllers/API/V4/PolicyController.php
+++ b/src/app/Http/Controllers/API/V4/PolicyController.php
@@ -1,150 +1,162 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use App\Policy\Greylist;
use App\Policy\Mailfilter;
use App\Policy\Password;
use App\Policy\RateLimit;
use App\Policy\SmtpAccess;
use App\Policy\SPF;
use App\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class PolicyController extends Controller
{
/**
* Validate the password regarding the defined policies.
*
* @return JsonResponse
*/
public function checkPassword(Request $request)
{
$userId = $request->input('user');
$user = !empty($userId) ? User::find($userId) : null;
// Check the password
$status = Password::checkPolicy($request->input('password'), $user, $user ? $user->walletOwner() : null);
$passed = array_filter(
$status,
static function ($rule) {
return !empty($rule['status']);
}
);
return response()->json([
'status' => count($passed) == count($status) ? 'success' : 'error',
'list' => array_values($status),
'count' => count($status),
]);
}
/**
* Take a greylist policy request
*
* @return JsonResponse
*/
public function greylist()
{
$response = Greylist::handle(\request()->input());
return $response->jsonResponse();
}
/**
* Fetch the account policies for the current user account.
* The result includes all supported policy rules.
*
* @return JsonResponse
*/
public function index(Request $request)
{
$user = $this->guard()->user();
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
$owner = $user->walletOwner();
if (!$user->canDelete($owner)) {
return $this->errorResponse(403);
}
$config = $owner->getConfig();
$policy_config = [];
// Get the password policies
$password_policy = Password::rules($owner, true);
$policy_config['max_password_age'] = $config['max_password_age'];
// Get the mail delivery policies
$mail_delivery_policy = ['greylist_policy'];
$policy_config['greylist_policy'] = $config['greylist_policy'];
if (config('app.with_mailfilter')) {
foreach (['itip_policy', 'externalsender_policy', 'externalsender_policy_domains'] as $name) {
$mail_delivery_policy[] = $name;
$policy_config[$name] = $config[$name];
}
}
return response()->json([
'password' => array_values($password_policy),
'mailDelivery' => $mail_delivery_policy,
'config' => $policy_config,
]);
}
/**
* SMTP Content Filter
*
* @param Request $request the API request
*
* @return Response The response
*/
public function mailfilter(Request $request)
{
return Mailfilter::handle($request);
}
/*
* Apply a sensible rate limitation to a request.
*
* @return JsonResponse
*/
public function ratelimit()
{
$response = RateLimit::handle(\request()->input());
return $response->jsonResponse();
}
+ /**
+ * Validate a mail reception request (includes greylisting)
+ *
+ * @return JsonResponse
+ */
+ public function reception()
+ {
+ $response = SmtpAccess::reception(\request()->input());
+
+ return $response->jsonResponse();
+ }
+
/*
* Apply the sender policy framework to a request.
*
* @return JsonResponse
*/
public function senderPolicyFramework()
{
$response = SPF::handle(\request()->input());
return $response->jsonResponse();
}
/*
* Validate sender/recipients in an SMTP submission request.
*
* @return JsonResponse
*/
public function submission()
{
$response = SmtpAccess::submission(\request()->input());
return $response->jsonResponse();
}
}
diff --git a/src/app/Policy/SmtpAccess.php b/src/app/Policy/SmtpAccess.php
index 225ca2c1..3b9f36e4 100644
--- a/src/app/Policy/SmtpAccess.php
+++ b/src/app/Policy/SmtpAccess.php
@@ -1,116 +1,187 @@
<?php
namespace App\Policy;
+use App\Group;
use App\User;
use App\UserAlias;
use App\Utils;
class SmtpAccess
{
+ /**
+ * Handle SMTP external mail reception request
+ *
+ * @param array $data Input data
+ */
+ public static function reception($data): Response
+ {
+ // Check access policy
+ if (!self::verifyRecipient($data['sender'], $data['recipient'])) {
+ return new Response(Response::ACTION_REJECT, 'Invalid recipient', 403);
+ }
+
+ // Greylisting
+ $response = Greylist::handle($data);
+
+ return $response;
+ }
+
/**
* Handle SMTP submission request
*
* @param array $data Input data
*/
public static function submission($data): Response
{
// TODO: The old SMTP access policy had an option ('empty_sender_hosts') to allow
// sending mail with no sender from configured networks.
[$local, $domain] = Utils::normalizeAddress($data['sender'], true);
if (empty($local) || empty($domain)) {
return new Response(Response::ACTION_REJECT, 'Invalid sender', 403);
}
$sender = $local . '@' . $domain;
[$local, $domain] = Utils::normalizeAddress($data['user'], true);
if (empty($local) || empty($domain)) {
return new Response(Response::ACTION_REJECT, 'Invalid user', 403);
}
$sasl_user = $local . '@' . $domain;
$user = User::where('email', $sasl_user)->first();
if (!$user) {
return new Response(Response::ACTION_REJECT, "Could not find user {$data['user']}", 403);
}
if (!self::verifySender($user, $sender)) {
$reason = "{$sasl_user} is unauthorized to send mail as {$sender}";
return new Response(Response::ACTION_REJECT, $reason, 403);
}
// TODO: should we be using the $user or the $sender?
$response = RateLimit::verifyRequest($user, (array) $data['recipients']);
if ($response->action != Response::ACTION_DUNNO) {
return $response;
}
// TODO: Prepending Sender/X-Sender/X-Authenticated-As headers?
// Leave it up to the postfix configuration how to proceed (accept would stop processing)
return new Response(Response::ACTION_DUNNO);
}
/**
* Verify whether a user is allowed to send using the envelope sender address.
*
* @param User $user Authenticated user
* @param string $email Email address
*/
public static function verifySender(User $user, string $email): bool
{
if ($user->isSuspended() || !str_contains($email, '@')) {
return false;
}
// TODO: Make sure the domain is not suspended
$email = \strtolower($email);
if ($user->email == $email) {
return true;
}
// noreply@ user can impersonate everyone
if ($user->email == \config('mail.mailers.smtp.username')) {
return true;
}
// Is it one of user's aliases?
$alias = $user->aliases()->where('alias', $email)->first();
if ($alias) {
return true;
}
// Delegation
if (\config('app.with_delegation')) {
// Is it another user's email?
$other_users = User::where('email', $email)->pluck('id')->all();
if (!count($other_users)) {
// Is it another user's alias?
$other_users = UserAlias::where('alias', $email)->pluck('user_id')->all();
}
if (count($other_users)) {
// Is the user a delegatee of that other user? Is he suspended?
$is_delegate = $user->delegators()->whereIn('user_id', $other_users)
->whereNot('users.status', '&', User::STATUS_SUSPENDED)
->exists();
if ($is_delegate) {
return true;
}
}
}
return false;
}
+
+ /**
+ * Verify whether a sender is allowed to send mail to the recipient address.
+ *
+ * @param string $sender Sender email address
+ * @param string $recipient Recipient email address
+ */
+ public static function verifyRecipient(string $sender, string $recipient): bool
+ {
+ $sender = \strtolower($sender);
+
+ if (!str_contains($sender, '@')) {
+ return false;
+ }
+
+ $group = Group::where('email', $recipient)->first();
+
+ // Check distribution list sender access list
+ if ($group) {
+ $policy = $group->getConfig()['sender_policy'];
+
+ if (!empty($policy)) {
+ foreach ($policy as $entry) {
+ // Full email address match
+ if (str_contains($entry, '@')) {
+ if ($sender === $entry) {
+ return true;
+ }
+ } else {
+ [$local, $domain] = explode('@', $sender);
+
+ // Domain suffix match
+ if (str_starts_with($entry, '.')) {
+ if (str_ends_with($domain, $entry)) {
+ return true;
+ }
+ }
+ // Full domain match
+ elseif ($entry === $domain) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+
+ // TODO: Check domain/recipient suspended status?
+
+ return true;
+ }
}
diff --git a/src/routes/api.php b/src/routes/api.php
index 19920662..041a860c 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,322 +1,323 @@
<?php
use App\Http\Controllers\API;
use Illuminate\Support\Facades\Route;
Route::get('health/readiness', [API\V4\HealthController::class, 'readiness']);
Route::get('health/liveness', [API\V4\HealthController::class, 'liveness']);
Route::post('oauth/approve', [API\AuthController::class, 'oauthApprove'])
->middleware(['auth:api']);
Route::group(
[
'middleware' => 'api',
'prefix' => 'auth',
],
static function () {
Route::post('login', [API\AuthController::class, 'login']);
Route::post('password-policy-check', [API\V4\PolicyController::class, 'checkPassword']);
Route::post('password-reset/init', [API\PasswordResetController::class, 'init']);
Route::post('password-reset/verify', [API\PasswordResetController::class, 'verify']);
Route::post('password-reset', [API\PasswordResetController::class, 'reset']);
Route::post('password-reset-expired', [API\PasswordResetController::class, 'resetExpired']);
if (\config('app.with_signup')) {
Route::get('signup/domains', [API\SignupController::class, 'domains']);
Route::post('signup/init', [API\SignupController::class, 'init']);
Route::get('signup/invitations/{id}', [API\SignupController::class, 'invitation']);
Route::get('signup/plans', [API\SignupController::class, 'plans']);
Route::post('signup/validate', [API\SignupController::class, 'signupValidate']);
Route::post('signup/verify', [API\SignupController::class, 'verify']);
Route::post('signup', [API\SignupController::class, 'signup']);
}
Route::group(
['middleware' => ['auth:api', 'scope:api']],
static function () {
Route::get('info', [API\AuthController::class, 'info']);
Route::post('info', [API\AuthController::class, 'info']);
Route::get('location', [API\AuthController::class, 'location']);
Route::post('logout', [API\AuthController::class, 'logout']);
Route::post('refresh', [API\AuthController::class, 'refresh']);
}
);
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => ['auth:api', 'scope:mfa,api'],
'prefix' => 'v4',
],
static function () {
Route::post('auth-attempts/{id}/confirm', [API\V4\AuthAttemptsController::class, 'confirm']);
Route::post('auth-attempts/{id}/deny', [API\V4\AuthAttemptsController::class, 'deny']);
Route::get('auth-attempts/{id}/details', [API\V4\AuthAttemptsController::class, 'details']);
Route::get('auth-attempts', [API\V4\AuthAttemptsController::class, 'index']);
Route::post('companion/register', [API\V4\CompanionAppsController::class, 'register']);
}
);
if (\config('app.with_files')) {
Route::group(
[
'middleware' => ($middleware = ['auth:api', 'scope:fs,api']),
'prefix' => 'v4',
],
static function () use ($middleware) {
Route::apiResource('fs', API\V4\FsController::class);
Route::get('fs/{itemId}/permissions', [API\V4\FsController::class, 'getPermissions']);
Route::post('fs/{itemId}/permissions', [API\V4\FsController::class, 'createPermission']);
Route::put('fs/{itemId}/permissions/{id}', [API\V4\FsController::class, 'updatePermission']);
Route::delete('fs/{itemId}/permissions/{id}', [API\V4\FsController::class, 'deletePermission']);
Route::post('fs/uploads/{id}', [API\V4\FsController::class, 'upload'])
->withoutMiddleware($middleware)->middleware(['api']);
Route::get('fs/downloads/{id}', [API\V4\FsController::class, 'download'])
->withoutMiddleware($middleware);
}
);
}
if (\config('app.with_admin')) {
Route::group(
[
'domain' => 'admin.' . \config('app.website_domain'),
'middleware' => ['auth:api', 'admin'],
'prefix' => 'v4',
],
static function () {
Route::apiResource('domains', API\V4\Admin\DomainsController::class);
Route::get('domains/{id}/skus', [API\V4\Admin\DomainsController::class, 'skus']);
Route::post('domains/{id}/suspend', [API\V4\Admin\DomainsController::class, 'suspend']);
Route::post('domains/{id}/unsuspend', [API\V4\Admin\DomainsController::class, 'unsuspend']);
Route::get('eventlog/{type}/{id}', [API\V4\Admin\EventLogController::class, 'index']);
Route::apiResource('groups', API\V4\Admin\GroupsController::class);
Route::post('groups/{id}/suspend', [API\V4\Admin\GroupsController::class, 'suspend']);
Route::post('groups/{id}/unsuspend', [API\V4\Admin\GroupsController::class, 'unsuspend']);
Route::apiResource('resources', API\V4\Admin\ResourcesController::class);
Route::apiResource('shared-folders', API\V4\Admin\SharedFoldersController::class);
Route::apiResource('skus', API\V4\Admin\SkusController::class);
Route::apiResource('users', API\V4\Admin\UsersController::class);
Route::get('users/{id}/discounts', [API\V4\Admin\DiscountsController::class, 'userDiscounts']);
Route::post('users/{id}/reset-2fa', [API\V4\Admin\UsersController::class, 'reset2FA']);
Route::post('users/{id}/reset-geolock', [API\V4\Admin\UsersController::class, 'resetGeoLock']);
Route::post('users/{id}/resync', [API\V4\Admin\UsersController::class, 'resync']);
Route::get('users/{id}/skus', [API\V4\Admin\UsersController::class, 'skus']);
Route::post('users/{id}/skus/{sku}', [API\V4\Admin\UsersController::class, 'setSku']);
Route::post('users/{id}/suspend', [API\V4\Admin\UsersController::class, 'suspend']);
Route::post('users/{id}/unsuspend', [API\V4\Admin\UsersController::class, 'unsuspend']);
Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
Route::post('wallets/{id}/one-off', [API\V4\Admin\WalletsController::class, 'oneOff']);
Route::get('wallets/{id}/receipts', [API\V4\Admin\WalletsController::class, 'receipts']);
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\Admin\WalletsController::class, 'receiptDownload']);
Route::get('wallets/{id}/transactions', [API\V4\Admin\WalletsController::class, 'transactions']);
Route::get('stats/chart/{chart}', [API\V4\Admin\StatsController::class, 'chart']);
Route::get('inspect-request', [API\V4\Admin\UsersController::class, 'inspectRequest'])
->withoutMiddleware(['auth:api', 'admin']);
}
);
}
if (\config('app.with_reseller')) {
Route::group(
[
'domain' => 'reseller.' . \config('app.website_domain'),
'middleware' => ['auth:api', 'reseller'],
'prefix' => 'v4',
],
static function () {
Route::apiResource('domains', API\V4\Reseller\DomainsController::class);
Route::get('domains/{id}/skus', [API\V4\Reseller\DomainsController::class, 'skus']);
Route::post('domains/{id}/suspend', [API\V4\Reseller\DomainsController::class, 'suspend']);
Route::post('domains/{id}/unsuspend', [API\V4\Reseller\DomainsController::class, 'unsuspend']);
Route::get('eventlog/{type}/{id}', [API\V4\Reseller\EventLogController::class, 'index']);
Route::apiResource('groups', API\V4\Reseller\GroupsController::class);
Route::post('groups/{id}/suspend', [API\V4\Reseller\GroupsController::class, 'suspend']);
Route::post('groups/{id}/unsuspend', [API\V4\Reseller\GroupsController::class, 'unsuspend']);
Route::apiResource('invitations', API\V4\Reseller\InvitationsController::class);
Route::post('invitations/{id}/resend', [API\V4\Reseller\InvitationsController::class, 'resend']);
Route::post('payments', [API\V4\Reseller\PaymentsController::class, 'store']);
Route::get('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandate']);
Route::post('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateCreate']);
Route::put('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateUpdate']);
Route::delete('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateDelete']);
Route::get('payments/methods', [API\V4\Reseller\PaymentsController::class, 'paymentMethods']);
Route::get('payments/pending', [API\V4\Reseller\PaymentsController::class, 'payments']);
Route::get('payments/has-pending', [API\V4\Reseller\PaymentsController::class, 'hasPayments']);
Route::apiResource('resources', API\V4\Reseller\ResourcesController::class);
Route::apiResource('shared-folders', API\V4\Reseller\SharedFoldersController::class);
Route::apiResource('skus', API\V4\Reseller\SkusController::class);
Route::apiResource('users', API\V4\Reseller\UsersController::class);
Route::get('users/{id}/discounts', [API\V4\Reseller\DiscountsController::class, 'userDiscounts']);
Route::post('users/{id}/reset-2fa', [API\V4\Reseller\UsersController::class, 'reset2FA']);
Route::post('users/{id}/reset-geolock', [API\V4\Reseller\UsersController::class, 'resetGeoLock']);
Route::post('users/{id}/resync', [API\V4\Reseller\UsersController::class, 'resync']);
Route::get('users/{id}/skus', [API\V4\Reseller\UsersController::class, 'skus']);
Route::post('users/{id}/skus/{sku}', [API\V4\Reseller\UsersController::class, 'setSku']);
Route::post('users/{id}/suspend', [API\V4\Reseller\UsersController::class, 'suspend']);
Route::post('users/{id}/unsuspend', [API\V4\Reseller\UsersController::class, 'unsuspend']);
Route::apiResource('wallets', API\V4\Reseller\WalletsController::class);
Route::post('wallets/{id}/one-off', [API\V4\Reseller\WalletsController::class, 'oneOff']);
Route::get('wallets/{id}/receipts', [API\V4\Reseller\WalletsController::class, 'receipts']);
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\Reseller\WalletsController::class, 'receiptDownload']);
Route::get('wallets/{id}/transactions', [API\V4\Reseller\WalletsController::class, 'transactions']);
Route::get('stats/chart/{chart}', [API\V4\Reseller\StatsController::class, 'chart']);
}
);
}
Route::group(
[
'middleware' => ['regularHosts', 'auth:api', 'scope:api'],
'prefix' => 'v4',
],
static function () {
Route::apiResource('companions', API\V4\CompanionAppsController::class);
// This must not be accessible with the 2fa token,
// to prevent an attacker from pairing a new device with a stolen token.
Route::get('companions/{id}/pairing', [API\V4\CompanionAppsController::class, 'pairing']);
Route::get('config/webmail', [API\V4\ConfigController::class, 'webmail']);
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', [API\V4\DomainsController::class, 'confirm']);
Route::get('domains/{id}/skus', [API\V4\DomainsController::class, 'skus']);
Route::get('domains/{id}/status', [API\V4\DomainsController::class, 'status']);
Route::post('domains/{id}/config', [API\V4\DomainsController::class, 'setConfig']);
Route::apiResource('groups', API\V4\GroupsController::class);
Route::get('groups/{id}/skus', [API\V4\GroupsController::class, 'skus']);
Route::get('groups/{id}/status', [API\V4\GroupsController::class, 'status']);
Route::post('groups/{id}/config', [API\V4\GroupsController::class, 'setConfig']);
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('rooms', API\V4\RoomsController::class);
Route::post('rooms/{id}/config', [API\V4\RoomsController::class, 'setConfig']);
Route::get('rooms/{id}/skus', [API\V4\RoomsController::class, 'skus']);
Route::post('meet/rooms/{id}', [API\V4\MeetController::class, 'joinRoom'])
->withoutMiddleware(['auth:api', 'scope:api']);
Route::apiResource('resources', API\V4\ResourcesController::class);
Route::get('resources/{id}/skus', [API\V4\ResourcesController::class, 'skus']);
Route::get('resources/{id}/status', [API\V4\ResourcesController::class, 'status']);
Route::post('resources/{id}/config', [API\V4\ResourcesController::class, 'setConfig']);
Route::apiResource('shared-folders', API\V4\SharedFoldersController::class);
Route::get('shared-folders/{id}/skus', [API\V4\SharedFoldersController::class, 'skus']);
Route::get('shared-folders/{id}/status', [API\V4\SharedFoldersController::class, 'status']);
Route::post('shared-folders/{id}/config', [API\V4\SharedFoldersController::class, 'setConfig']);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
Route::post('users/{id}/config', [API\V4\UsersController::class, 'setConfig']);
Route::post('users/{id}/login-as', [API\V4\UsersController::class, 'loginAs']);
Route::get('users/{id}/skus', [API\V4\UsersController::class, 'skus']);
Route::get('users/{id}/status', [API\V4\UsersController::class, 'status']);
if (\config('app.with_delegation')) {
Route::get('users/{id}/delegations', [API\V4\UsersController::class, 'delegations']);
Route::post('users/{id}/delegations', [API\V4\UsersController::class, 'createDelegation']);
Route::delete('users/{id}/delegations/{email}', [API\V4\UsersController::class, 'deleteDelegation']);
Route::get('users/{id}/delegators', [API\V4\UsersController::class, 'delegators']);
}
Route::apiResource('wallets', API\V4\WalletsController::class);
Route::get('wallets/{id}/transactions', [API\V4\WalletsController::class, 'transactions']);
Route::get('wallets/{id}/receipts', [API\V4\WalletsController::class, 'receipts']);
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\WalletsController::class, 'receiptDownload']);
Route::get('wallets/{id}/referral-programs', [API\V4\WalletsController::class, 'referralPrograms']);
Route::get('policies', [API\V4\PolicyController::class, 'index']);
Route::post('password-reset/code', [API\PasswordResetController::class, 'codeCreate']);
Route::delete('password-reset/code/{id}', [API\PasswordResetController::class, 'codeDelete']);
Route::post('payments', [API\V4\PaymentsController::class, 'store']);
// Route::delete('payments', [API\V4\PaymentsController::class, 'cancel']);
Route::get('payments/mandate', [API\V4\PaymentsController::class, 'mandate']);
Route::post('payments/mandate', [API\V4\PaymentsController::class, 'mandateCreate']);
Route::put('payments/mandate', [API\V4\PaymentsController::class, 'mandateUpdate']);
Route::delete('payments/mandate', [API\V4\PaymentsController::class, 'mandateDelete']);
Route::post('payments/mandate/reset', [API\V4\PaymentsController::class, 'mandateReset']);
Route::get('payments/methods', [API\V4\PaymentsController::class, 'paymentMethods']);
Route::get('payments/pending', [API\V4\PaymentsController::class, 'payments']);
Route::get('payments/has-pending', [API\V4\PaymentsController::class, 'hasPayments']);
Route::get('payments/status', [API\V4\PaymentsController::class, 'paymentStatus']);
Route::get('search/self', [API\V4\SearchController::class, 'searchSelf']);
Route::get('search/contacts', [API\V4\SearchController::class, 'searchContacts']);
Route::get('search/user', [API\V4\SearchController::class, 'searchUser']);
Route::post('support/request', [API\V4\SupportController::class, 'request'])
->withoutMiddleware(['auth:api', 'scope:api'])
->middleware(['api']);
Route::get('vpn/token', [API\V4\VPNController::class, 'token']);
Route::get('license/{type}', [API\V4\LicenseController::class, 'license']);
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'prefix' => 'webhooks',
],
static function () {
Route::post('payment/{provider}', [API\V4\PaymentsController::class, 'webhook']);
Route::post('meet', [API\V4\MeetController::class, 'webhook']);
}
);
if (\config('app.with_services')) {
Route::group(
[
'prefix' => 'webhooks',
],
static function () {
Route::get('metrics/swoole', [API\V4\MetricsController::class, 'swooleMetrics']);
}
);
Route::group(
[
'middleware' => ['allowedHosts'],
'prefix' => 'webhooks',
],
static function () {
Route::get('nginx', [API\V4\NGINXController::class, 'authenticate']);
Route::get('nginx-roundcube', [API\V4\NGINXController::class, 'authenticateRoundcube']);
Route::get('nginx-httpauth', [API\V4\NGINXController::class, 'httpauth']);
Route::post('cyrus-sasl', [API\V4\NGINXController::class, 'cyrussasl']);
Route::get('metrics', [API\V4\MetricsController::class, 'metrics']);
Route::post('policy/greylist', [API\V4\PolicyController::class, 'greylist']);
Route::post('policy/ratelimit', [API\V4\PolicyController::class, 'ratelimit']);
Route::post('policy/spf', [API\V4\PolicyController::class, 'senderPolicyFramework']);
+ Route::post('policy/reception', [API\V4\PolicyController::class, 'reception']);
Route::post('policy/submission', [API\V4\PolicyController::class, 'submission']);
Route::post('policy/mail/filter', [API\V4\PolicyController::class, 'mailfilter']);
}
);
}
diff --git a/src/tests/Feature/Controller/PolicyTest.php b/src/tests/Feature/Controller/PolicyTest.php
index d10aa598..8e8a8fb0 100644
--- a/src/tests/Feature/Controller/PolicyTest.php
+++ b/src/tests/Feature/Controller/PolicyTest.php
@@ -1,459 +1,513 @@
<?php
namespace Tests\Feature\Controller;
use App\Domain;
use App\IP4Net;
use App\Policy\Greylist;
+use App\Policy\Mailfilter;
use Carbon\Carbon;
use Tests\TestCase;
class PolicyTest extends TestCase
{
private $clientAddress;
private $net;
private $testUser;
private $testDomain;
protected function setUp(): void
{
parent::setUp();
$this->clientAddress = '128.0.0.100';
$this->net = IP4Net::create([
'net_number' => '128.0.0.0',
'net_broadcast' => '128.255.255.255',
'net_mask' => 8,
'country' => 'US',
'rir_name' => 'test',
'serial' => 1,
]);
$this->testDomain = $this->getTestDomain('test.domain', [
'type' => Domain::TYPE_EXTERNAL,
'status' => Domain::STATUS_ACTIVE | Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED,
]);
$this->testUser = $this->getTestUser('john@test.domain');
Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
Greylist\Whitelist::where('sender_domain', 'sender.domain')->delete();
$this->useServicesUrl();
}
protected function tearDown(): void
{
+ $this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestUser($this->testUser->email);
$this->deleteTestDomain($this->testDomain->namespace);
$this->net->delete();
Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
Greylist\Whitelist::where('sender_domain', 'sender.domain')->delete();
$john = $this->getTestUser('john@kolab.org');
$john->settings()
->whereIn('key', ['password_policy', 'max_password_age', 'itip_policy', 'externalsender_policy'])->delete();
parent::tearDown();
}
/**
* Test password policy check
*/
public function testCheckPassword(): void
{
$this->useRegularUrl();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('password_policy', 'min:8,max:100,upper,digit');
// Empty password
$post = ['user' => $john->id];
$response = $this->post('/api/auth/password-policy-check', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(3, $json);
$this->assertSame('error', $json['status']);
$this->assertSame(4, $json['count']);
$this->assertFalse($json['list'][0]['status']);
$this->assertSame('min', $json['list'][0]['label']);
$this->assertFalse($json['list'][1]['status']);
$this->assertSame('max', $json['list'][1]['label']);
$this->assertFalse($json['list'][2]['status']);
$this->assertSame('upper', $json['list'][2]['label']);
$this->assertFalse($json['list'][3]['status']);
$this->assertSame('digit', $json['list'][3]['label']);
// Test acting as Jack, password non-compliant
$post = ['password' => '9999999', 'user' => $jack->id];
$response = $this->post('/api/auth/password-policy-check', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(3, $json);
$this->assertSame('error', $json['status']);
$this->assertSame(4, $json['count']);
$this->assertFalse($json['list'][0]['status']); // min
$this->assertTrue($json['list'][1]['status']); // max
$this->assertFalse($json['list'][2]['status']); // upper
$this->assertTrue($json['list'][3]['status']); // digit
// Test with no user context, expect use of the default policy
$post = ['password' => '9'];
$response = $this->post('/api/auth/password-policy-check', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(3, $json);
$this->assertSame('error', $json['status']);
$this->assertSame(2, $json['count']);
$this->assertFalse($json['list'][0]['status']);
$this->assertSame('min', $json['list'][0]['label']);
$this->assertTrue($json['list'][1]['status']);
$this->assertSame('max', $json['list'][1]['label']);
}
/**
* Test greylist policy webhook
*/
public function testGreylist()
{
// Note: Only basic tests here. More detailed policy handler tests are in another place
// Test 403 response
$post = [
'sender' => 'someone@sender.domain',
'recipient' => $this->testUser->email,
'client_address' => $this->clientAddress,
'client_name' => 'some.mx',
];
$response = $this->post('/api/webhooks/policy/greylist', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('DEFER_IF_PERMIT', $json['response']);
$this->assertSame("Greylisted for 5 minutes. Try again later.", $json['reason']);
// Test 200 response
$connect = Greylist\Connect::where('sender_domain', 'sender.domain')->first();
$connect->created_at = Carbon::now()->subMinutes(6);
$connect->save();
$response = $this->post('/api/webhooks/policy/greylist', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('DUNNO', $json['response']);
$this->assertMatchesRegularExpression('/^Received-Greylist: greylisted from/', $json['prepend'][0]);
}
/**
* Test fetching account 'password' policies
*/
public function testIndexPassword(): void
{
$this->useRegularUrl();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('password_policy', 'min:8,max:255,special');
$john->setSetting('max_password_age', 6);
// Unauth access not allowed
$response = $this->get('/api/v4/policies');
$response->assertStatus(401);
// Test acting as non-controller
$response = $this->actingAs($jack)->get('/api/v4/policies');
$response->assertStatus(403);
// Get available policy rules
$response = $this->actingAs($john)->get('/api/v4/policies');
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(7, $json['password']);
$this->assertSame('6', $json['config']['max_password_age']);
$this->assertSame('Minimum password length: 8 characters', $json['password'][0]['name']);
$this->assertSame('min', $json['password'][0]['label']);
$this->assertSame('8', $json['password'][0]['param']);
$this->assertTrue($json['password'][0]['enabled']);
$this->assertSame('Maximum password length: 255 characters', $json['password'][1]['name']);
$this->assertSame('max', $json['password'][1]['label']);
$this->assertSame('255', $json['password'][1]['param']);
$this->assertTrue($json['password'][1]['enabled']);
$this->assertSame('lower', $json['password'][2]['label']);
$this->assertFalse($json['password'][2]['enabled']);
$this->assertSame('upper', $json['password'][3]['label']);
$this->assertFalse($json['password'][3]['enabled']);
$this->assertSame('digit', $json['password'][4]['label']);
$this->assertFalse($json['password'][4]['enabled']);
$this->assertSame('special', $json['password'][5]['label']);
$this->assertTrue($json['password'][5]['enabled']);
$this->assertSame('last', $json['password'][6]['label']);
$this->assertFalse($json['password'][6]['enabled']);
}
/**
* Test fetching account 'mailDelivery' policies
*/
public function testIndexMailDelivery(): void
{
$this->useRegularUrl();
$keys = ['greylist_policy', 'itip_policy', 'externalsender_policy', 'externalsender_policy_domains'];
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$john->settings()->whereIn('key', $keys)->delete();
// Unauth access not allowed
$response = $this->get('/api/v4/policies');
$response->assertStatus(401);
// Test acting as non-controller
$response = $this->actingAs($jack)->get('/api/v4/policies');
$response->assertStatus(403);
// Get polcies when mailfilter is disabled
\config(['app.with_mailfilter' => false]);
$response = $this->actingAs($john)->get('/api/v4/policies');
$json = $response->json();
$response->assertStatus(200);
$this->assertSame(['greylist_policy'], $json['mailDelivery']);
$this->assertTrue($json['config']['greylist_policy']);
// Get polcies when mailfilter is enabled
\config(['app.with_mailfilter' => true]);
$john->setConfig(['externalsender_policy' => true]);
$response = $this->actingAs($john)->get('/api/v4/policies');
$json = $response->json();
$response->assertStatus(200);
$this->assertSame($keys, $json['mailDelivery']);
$this->assertFalse($json['config']['itip_policy']);
$this->assertTrue($json['config']['greylist_policy']);
$this->assertTrue($json['config']['externalsender_policy']);
$this->assertSame([], $json['config']['externalsender_policy_domains']);
}
/**
* Test mail filter (POST /api/webhooks/policy/mail/filter)
*/
public function testMailfilter()
{
// Note: Only basic tests here. More detailed policy handler tests are in another place
$headers = ['CONTENT_TYPE' => 'message/rfc822'];
$post = file_get_contents(self::BASE_DIR . '/data/mail/1.eml');
$post = str_replace("\n", "\r\n", $post);
$john = $this->getTestUser('john@kolab.org');
// Basic test, no changes to the mail content
$url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org&sender=jack@kolab.org';
$response = $this->call('POST', $url, [], [], [], $headers, $post)
- ->assertNoContent(204);
+ ->assertNoContent(200)
+ ->assertHeader(Mailfilter::HEADER, Mailfilter::HEADER_ACTION_ACCEPT_EMPTY);
// Test returning (modified) mail content
$john->setConfig(['externalsender_policy' => true, 'itip_policy' => true]);
$url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org&sender=jack@external.tld';
$content = $this->call('POST', $url, [], [], [], $headers, $post)
->assertStatus(200)
->assertHeader('Content-Type', 'message/rfc822')
->streamedContent();
$this->assertStringContainsString('Subject: [EXTERNAL] test sync', $content);
}
+ /**
+ * Test mail reception policy webhook
+ */
+ public function testReception()
+ {
+ // Note: Only basic tests here. More detailed policy handler tests are in another place
+
+ // Test 403 response
+ $post = [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->testUser->email,
+ 'client_address' => $this->clientAddress,
+ 'client_name' => 'some.mx',
+ ];
+
+ $response = $this->post('/api/webhooks/policy/reception', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertSame('DEFER_IF_PERMIT', $json['response']);
+ $this->assertSame("Greylisted for 5 minutes. Try again later.", $json['reason']);
+
+ // Test 200 response
+ $connect = Greylist\Connect::where('sender_domain', 'sender.domain')->first();
+ $connect->created_at = Carbon::now()->subMinutes(6);
+ $connect->save();
+
+ $response = $this->post('/api/webhooks/policy/reception', $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('DUNNO', $json['response']);
+ $this->assertMatchesRegularExpression('/^Received-Greylist: greylisted from/', $json['prepend'][0]);
+
+ // Test sender access check (403)
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->setConfig(['sender_policy' => ['aaa.pl']]);
+
+ $post['recipient'] = $group->email;
+ $response = $this->post('/api/webhooks/policy/reception', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('REJECT', $json['response']);
+ $this->assertSame('Invalid recipient', $json['reason']);
+ }
+
/**
* Test submission policy webhook
*/
public function testSubmission()
{
// Note: Only basic tests here. More detailed policy handler tests are in another place
// Test invalid sender
$post = [
'sender' => 'sender',
'recipients' => ['recipient@gmail.com'],
];
$response = $this->post('/api/webhooks/policy/submission', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('REJECT', $json['response']);
$this->assertSame("Invalid sender", $json['reason']);
// Test invalid user
$post = [
'user' => 'unknown',
'sender' => $this->testUser->email,
'recipients' => ['recipient@gmail.com'],
];
$response = $this->post('/api/webhooks/policy/submission', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('REJECT', $json['response']);
$this->assertSame("Invalid user", $json['reason']);
// Test unknown user
$post = [
'user' => 'unknown@domain.tld',
'sender' => 'john+test@test.domain',
'recipients' => ['recipient@gmail.com'],
];
$response = $this->post('/api/webhooks/policy/submission', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('REJECT', $json['response']);
$this->assertSame("Could not find user {$post['user']}", $json['reason']);
// Test existing user and an invalid sender address
$post = [
'user' => 'john@test.domain',
'sender' => 'john1@test.domain',
'recipients' => ['recipient@gmail.com'],
];
$response = $this->post('/api/webhooks/policy/submission', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('REJECT', $json['response']);
$this->assertSame("john@test.domain is unauthorized to send mail as john1@test.domain", $json['reason']);
// Test existing user with a valid sender address
$post = [
'user' => 'john@test.domain',
'sender' => 'john+test@test.domain',
'recipients' => ['recipient@gmail.com'],
];
$response = $this->post('/api/webhooks/policy/submission', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('DUNNO', $json['response']);
}
/**
* Test ratelimit policy webhook
*/
public function testRatelimit()
{
// Note: Only basic tests here. More detailed policy handler tests are in another place
// Test a valid user
$post = [
'sender' => $this->testUser->email,
'recipients' => 'someone@sender.domain',
];
$response = $this->post('/api/webhooks/policy/ratelimit', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('DUNNO', $json['response']);
// Test invalid sender
$post['sender'] = 'non-existing';
$response = $this->post('/api/webhooks/policy/ratelimit', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('HOLD', $json['response']);
$this->assertSame('Invalid sender email', $json['reason']);
// Test unknown sender
$post['sender'] = 'non-existing@example.com';
$response = $this->post('/api/webhooks/policy/ratelimit', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('DUNNO', $json['response']);
// Test alias sender
$this->testUser->suspend();
$this->testUser->aliases()->create(['alias' => 'alias@test.domain']);
$post['sender'] = 'alias@test.domain';
$response = $this->post('/api/webhooks/policy/ratelimit', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('HOLD', $json['response']);
$this->assertSame('Sender deleted or suspended', $json['reason']);
// Test app.ratelimit_whitelist
\config(['app.ratelimit_whitelist' => ['alias@test.domain']]);
$response = $this->post('/api/webhooks/policy/ratelimit', $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('DUNNO', $json['response']);
}
/**
* Test SPF webhook
*
* @group data
* @group skipci
*/
public function testSenderPolicyFramework(): void
{
// Note: Only basic tests here. More detailed policy handler tests are in another place
// TODO: Make a test that does not depend on data/dns (remove skipci)
// Test a valid user
$post = [
'instance' => 'test.local.instance',
'protocol_state' => 'RCPT',
'sender' => 'sender@spf-fail.kolab.org',
'client_name' => 'mx.kolabnow.com',
'client_address' => '212.103.80.148',
'recipient' => $this->testUser->email,
];
$response = $this->post('/api/webhooks/policy/spf', $post);
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('REJECT', $json['response']);
$this->assertSame('Prohibited by Sender Policy Framework', $json['reason']);
$this->assertSame(['Received-SPF: Fail identity=mailfrom; client-ip=212.103.80.148;'
. ' helo=mx.kolabnow.com; envelope-from=sender@spf-fail.kolab.org;'], $json['prepend']);
}
}
diff --git a/src/tests/Feature/Policy/SmtpAccessTest.php b/src/tests/Feature/Policy/SmtpAccessTest.php
index 79afd4bc..f8d75357 100644
--- a/src/tests/Feature/Policy/SmtpAccessTest.php
+++ b/src/tests/Feature/Policy/SmtpAccessTest.php
@@ -1,86 +1,118 @@
<?php
namespace Tests\Feature\Policy;
use App\Delegation;
use App\Policy\SmtpAccess;
use App\User;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class SmtpAccessTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Delegation::query()->delete();
$john = $this->getTestUser('john@kolab.org');
$john->status &= ~User::STATUS_SUSPENDED;
$john->save();
$jack = $this->getTestUser('jack@kolab.org');
$jack->status &= ~User::STATUS_SUSPENDED;
$jack->save();
}
protected function tearDown(): void
{
+ $this->deleteTestGroup('group-test@kolab.org');
Delegation::query()->delete();
$john = $this->getTestUser('john@kolab.org');
$john->status &= ~User::STATUS_SUSPENDED;
$john->save();
$jack = $this->getTestUser('jack@kolab.org');
$jack->status &= ~User::STATUS_SUSPENDED;
$jack->save();
parent::tearDown();
}
+ /**
+ * Test verifyRecipient() method
+ */
+ public function testVerifyRecipient(): void
+ {
+ $group = $this->getTestGroup('group-test@kolab.org');
+
+ // invalid sender address
+ $this->assertFalse(SmtpAccess::verifyRecipient('invalid', 'none@unknown.tld'));
+
+ // non-existing recipient
+ $this->assertTrue(SmtpAccess::verifyRecipient('ext@gmail.com', 'none@unknown.tld'));
+
+ // no policy for a group
+ $this->assertTrue(SmtpAccess::verifyRecipient('ext@gmail.com', $group->email));
+
+ $group->setConfig(['sender_policy' => ['.gmail.com', 'allowed.tld', 'allowed@kolab.org']]);
+
+ // domain suffix match
+ $this->assertTrue(SmtpAccess::verifyRecipient('ext@test.gmail.com', $group->email));
+
+ // domain match
+ $this->assertTrue(SmtpAccess::verifyRecipient('ext@allowed.tld', $group->email));
+
+ // email address match
+ $this->assertTrue(SmtpAccess::verifyRecipient('allowed@kolab.org', $group->email));
+
+ // no match
+ $this->assertFalse(SmtpAccess::verifyRecipient('test@kolab.ch', $group->email));
+ }
+
/**
* Test verifySender() method
*/
public function testVerifySender(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$noreply = User::where('email', \config('mail.mailers.smtp.username'))->first();
// Test main email address
$this->assertTrue(SmtpAccess::verifySender($john, ucfirst($john->email)));
// Test noreply@ user
if ($noreply) {
$this->assertTrue(SmtpAccess::verifySender($noreply, $john->email));
}
// Test an alias
$this->assertTrue(SmtpAccess::verifySender($john, 'John.Doe@kolab.org'));
// Test another user's email address
$this->assertFalse(SmtpAccess::verifySender($jack, $john->email));
// Test another user's alias
$this->assertFalse(SmtpAccess::verifySender($jack, 'john.doe@kolab.org'));
Queue::fake();
Delegation::create(['user_id' => $john->id, 'delegatee_id' => $jack->id]);
// Test delegator's email address
$this->assertTrue(SmtpAccess::verifySender($jack, $john->email));
// Test delegator's alias
$this->assertTrue(SmtpAccess::verifySender($jack, 'john.doe@kolab.org'));
// Test delegator's alias, but suspended delegator
$john->suspend();
$this->assertFalse(SmtpAccess::verifySender($jack, 'john.doe@kolab.org'));
// Test invalid/unknown email
$this->assertFalse(SmtpAccess::verifySender($jack, 'unknown'));
$this->assertFalse(SmtpAccess::verifySender($jack, 'unknown@domain.tld'));
// Test suspended user
$jack->suspend();
$this->assertFalse(SmtpAccess::verifySender($jack, $jack->email));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 6, 3:06 AM (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832136
Default Alt Text
(87 KB)

Event Timeline