Commit 9872d369 authored by mh's avatar mh
Browse files

start tracking mailboxes that are over quota

Introduce a flag for mailboxes that indicate whether this mailbox
is over quota or not.

The flag can be toggled through a dedicated endpoint that will be
triggered through dovecot.
Include current quota status in mailbox info and especially when
asking for a mailbox, so message delivery can be deferred at the
edge.

See https://doc.dovecot.org/configuration_manual/quota/ for background.
parent b057f01c
Pipeline #8605 passed with stage
in 2 minutes and 39 seconds
class MailboxOverQuota < ActiveRecord::Migration[5.2]
def change
add_column :email_users, :over_quota, :boolean, default: false
end
end
......@@ -37,10 +37,18 @@ class Authenticator
'post' => [
'/mail/mailbox',
'/mail/update_storagehost',
'/mail/update_over_quota',
'/auth/mailstore',
],
}
},
'MailQuotaReport' => {
:no_api_token => {
'post' => [
'/mail/update_over_quota',
],
},
},
# Mail relays need to know about routing (TODO) and do smtp auth
'Mailrelay' => {
:no_api_token => {
......
......@@ -15,6 +15,8 @@ class SystemJobsStatsGen
all_active: EmailIaddress.active.count,
deleted: EmailIaddress.deleted.count,
over_quota: EmailUser.over_quota.count,
mailboxes: EmailUser.active.count,
forwards: EmailForward.select(:email_iaddress_id).distinct.count,
aliases: EmailAlias.count,
......
......@@ -227,6 +227,7 @@ class AuthManager
email: user.email,
uid: user.uid,
quota: user.mbxquota,
over_quota: user.over_quota,
locked: user.locked?,
possible_resources: user.possible_resources,
inbox_namespace_prefix: user.inbox_namespace_prefix,
......
......@@ -47,6 +47,8 @@ class EmailUser < ActiveRecord::Base
scope :deleted, -> { EmailUser.joins(:email_iaddress).where("email_iaddresses.deleted_at IS NOT NULL") }
scope :locked, -> { active.where('locked_until IS NOT NULL AND locked_until != \'\'') }
scope :over_quota, -> { active.where(over_quota: true) }
before_validation :default_values
class << self
......
......@@ -259,13 +259,14 @@ class IApi < Sinatra::Base
end
IApiLog.info("Successful mailstore auth (userdb: #{userdb}) #{user[:log]}")
home = File.join('/var/mail/mails', user[:email])
extra = ['userdb_quota_rule', 'userdb_uid', 'userdb_gid',
extra = ['userdb_quota_rule', 'userdb_quota_over_flag', 'userdb_uid', 'userdb_gid',
'userdb_uid', 'userdb_gid', 'userdb_home',
'userdb_namespace/inbox/prefix']
res = {USER: user[:email],
HOME: home,
userdb_quota_rule: "*:storage=#{user[:quota]}",
userdb_quota_over_flag: user[:over_quota] ? 'true' : 'false',
userdb_uid: "#{user[:uid]}",
userdb_gid: '12',
userdb_home: home,
......
......@@ -24,6 +24,25 @@ class IApi < Sinatra::Base
client_error('failed')
end
post '/update_over_quota' do
email = validate_email(parsed_body['address'])
value = parsed_body['over_quota'].to_s
if ['true','false','mismatch'].include?(value)
if user = EmailUser.active_by_email(email)
v = if value == 'mismatch'
!user.over_quota
else
(value == 'true')
end
user.update!(over_quota: v)
return json result: 'success'
else
client_error('notfound')
end
end
client_error('failed')
end
post '/mailbox_or_forward' do
if email = validate_email(parsed_body['address'])
if u = MailManager.mailbox_or_forward?(email)
......@@ -64,6 +83,7 @@ class IApi < Sinatra::Base
result: 'success',
email: u.email,
uid: u.uid,
over_quota: u.over_quota,
storagehost: u.storagehost,
domain: u.domain,
}
......
......@@ -21,7 +21,7 @@ describe "iApi auth route" do
expect(login_data['userdb_uid']).to eql(user.uid.to_s)
expect(login_data['userdb_gid']).to eql("12") # hardcoded
expect(login_data['userdb_namespace/inbox/prefix']).to be_empty
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix")
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix")
end
it 'migrates mailbox to mailcrypt' do
email = 'user1@example.com'
......@@ -45,7 +45,7 @@ describe "iApi auth route" do
expect(login_data['userdb_mail_crypt_save_version']).to eql("2")
expect(login_data['userdb_mail_crypt_global_public_key']).to be_present
expect(login_data['userdb_mail_crypt_global_private_key']).to be_present
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key userdb_mail_crypt_global_private_key")
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key userdb_mail_crypt_global_private_key")
end
end
context 'to userdb lookup' do
......@@ -66,7 +66,7 @@ describe "iApi auth route" do
expect(login_data['userdb_uid']).to eql(user.uid.to_s)
expect(login_data['userdb_gid']).to eql("12") # hardcoded
expect(login_data['userdb_namespace/inbox/prefix']).to be_empty
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix")
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix")
end
it 'does not try to enable mail_crypt' do
email = 'user1@example.com'
......@@ -90,7 +90,7 @@ describe "iApi auth route" do
expect(login_data['userdb_uid']).to eql(user.uid.to_s)
expect(login_data['userdb_gid']).to eql("12") # hardcoded
expect(login_data['userdb_namespace/inbox/prefix']).to be_empty
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix")
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix")
end
it 'includes additional fields when mail_crypt enabled' do
email = 'user1@example.com'
......@@ -121,7 +121,7 @@ describe "iApi auth route" do
expect(login_data['userdb_mail_crypt_save_version']).to eql("2")
expect(login_data['userdb_mail_crypt_global_public_key']).to be_present
expect(login_data['userdb_mail_crypt_global_private_key']).to be_nil
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key")
expect(login_data['EXTRA']).to eql("userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key")
end
end
end
......
require File.expand_path '../spec_helper.rb', __FILE__
describe "iApi Mail" do
context '/update_over_quota' do
context 'as mailstore' do
it 'will update over_quota for a mailbox' do
email = 'user1@example.com'
expect(EmailUser.by_email(email)).not_to be_over_quota
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: true})
expect(last_response).to be_ok
expect(EmailUser.by_email(email)).to be_over_quota
end
it 'will update over_quota back for a mailbox' do
email = 'user1@example.com'
EmailUser.by_email(email).update!(over_quota: true)
expect(EmailUser.by_email(email)).to be_over_quota
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: false})
expect(last_response).to be_ok
expect(EmailUser.by_email(email)).not_to be_over_quota
end
it 'will switch over_quota for a mailbox on mismatch' do
email = 'user1@example.com'
expect(EmailUser.by_email(email)).not_to be_over_quota
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: 'mismatch'})
expect(last_response).to be_ok
expect(EmailUser.by_email(email)).to be_over_quota
end
it 'will switch over_quota for a mailbox on mismatch on false' do
email = 'user1@example.com'
EmailUser.by_email(email).update!(over_quota: true)
expect(EmailUser.by_email(email)).to be_over_quota
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: 'mismatch'})
expect(last_response).to be_ok
expect(EmailUser.by_email(email)).not_to be_over_quota
end
it 'will work as also on change' do
email = 'user1@example.com'
EmailUser.by_email(email).update!(over_quota: true)
expect(EmailUser.by_email(email)).to be_over_quota
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: true})
expect(last_response).to be_ok
expect(EmailUser.by_email(email)).to be_over_quota
end
it 'fails on unknown email' do
email = 'nouser@example.com'
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: true})
expect(last_response).to be_client_error
expect(last_response.parsed_body['errors']).to eql('notfound')
end
it 'fails on invalid value' do
email = 'nouser@example.com'
post_as('mailstore', '/mail/update_over_quota',{address: email, over_quota: 'meeh'})
expect(last_response).to be_client_error
expect(last_response.parsed_body['errors']).to eql('failed')
end
end
context 'as any' do
it "should not allow access" do
post_as_any('/mail/update_over_quota',{address: 'user1@example.com', over_quota: false })
expect(last_response).to be_unauthorized
end
end
end
context '/mailbox_routing' do
context 'as mailfrontend' do
it 'will give backend infos' do
......@@ -118,6 +189,7 @@ describe "iApi Mail" do
expect(user_data['storagehost']).to eql(user.storagehost)
expect(user_data['uid']).to eql(user.uid)
expect(user_data['domain']).to eql(user.domain)
expect(user_data['over_quota']).to be_falsey
end
it 'will give backend infos also for forward mailboxes' do
email = 'fwd1@example.com'
......
......@@ -119,7 +119,7 @@ module IAPI
[UserCert, UserKey]
elsif what =~ /admin/ || what =~ /invites\/generate/ || what =~ /resource\/.+/ || what =~ /domains\/list/
[AdminCert, AdminKey]
elsif what =~ /mailstore/
elsif what =~ /^\/auth\/mailstore/ || what =~ /^\/mail\/(update_over_quota|mailbox)/
[MailCert, MailKey]
elsif what =~ /^\/auth/
[LoginCert, LoginKey]
......
......@@ -112,9 +112,7 @@
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"hansjakobli@example.com","HOME":"/var/mail/mails/hansjakobli@example.com","userdb_quota_rule":"*:storage=1G","userdb_uid":"%UID%","userdb_gid":"12","userdb_home":"/var/mail/mails/hansjakobli@example.com","userdb_namespace/inbox/prefix":"","userdb_mail_crypt_save_version":"2","userdb_mail_crypt_global_public_key":"%PUBKEY%","userdb_mail_crypt_global_private_key":"%PRIVATEKEY%","EXTRA":"userdb_quota_rule
userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix
userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key userdb_mail_crypt_global_private_key"}}'
- '{"result":"success","login":{"USER":"hansjakobli@example.com","HOME":"/var/mail/mails/hansjakobli@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"false","userdb_uid":"%UID%","userdb_gid":"12","userdb_home":"/var/mail/mails/hansjakobli@example.com","userdb_namespace/inbox/prefix":"","userdb_mail_crypt_save_version":"2","userdb_mail_crypt_global_public_key":"%PUBKEY%","userdb_mail_crypt_global_private_key":"%PRIVATEKEY%","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key userdb_mail_crypt_global_private_key"}}'
---
:request:
:method: :get
......@@ -207,9 +205,7 @@
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"hansjakobli@example.com","HOME":"/var/mail/mails/hansjakobli@example.com","userdb_quota_rule":"*:storage=1G","userdb_uid":"%UID%","userdb_gid":"12","userdb_home":"/var/mail/mails/hansjakobli@example.com","userdb_namespace/inbox/prefix":"","userdb_mail_crypt_save_version":"2","userdb_mail_crypt_global_public_key":"%PUBKEY%","userdb_mail_crypt_global_private_key":"%PRIVATEKEY%","EXTRA":"userdb_quota_rule
userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix
userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key userdb_mail_crypt_global_private_key"}}'
- '{"result":"success","login":{"USER":"hansjakobli@example.com","HOME":"/var/mail/mails/hansjakobli@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"false","userdb_uid":"%UID%","userdb_gid":"12","userdb_home":"/var/mail/mails/hansjakobli@example.com","userdb_namespace/inbox/prefix":"","userdb_mail_crypt_save_version":"2","userdb_mail_crypt_global_public_key":"%PUBKEY%","userdb_mail_crypt_global_private_key":"%PRIVATEKEY%","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix userdb_mail_crypt_save_version userdb_mail_crypt_global_public_key userdb_mail_crypt_global_private_key"}}'
---
:request:
:method: :post
......
......@@ -8,5 +8,4 @@
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule
userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"false","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
---
:request:
:method: :post
:path: "/auth/mailstore"
:payload:
email: user1@example.com
password: pwduser1
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"false","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
---
:request:
:method: :post
:path: "/mail/update_over_quota"
:payload:
address: user1@example.com
over_quota: true
:response:
:status: 200
:body:
- '{"result":"success"}'
---
:request:
:method: :post
:path: "/mail/mailbox"
:payload:
address: user1@example.com
:response:
:status: 200
:body:
- '{"result":"success","email":"user1@example.com", "uid":44242, "over_quota":true, "storagehost":"storage1.example.com", "domain":"example.com"}'
---
:request:
:method: :post
:path: "/auth/mailstore"
:payload:
email: user1@example.com
password: pwduser1
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"true","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
---
:request:
:method: :post
:path: "/mail/update_over_quota"
:payload:
address: user1@example.com
over_quota: false
:response:
:status: 200
:body:
- '{"result":"success"}'
---
:request:
:method: :post
:path: "/mail/mailbox"
:payload:
address: user1@example.com
:response:
:status: 200
:body:
- '{"result":"success","email":"user1@example.com", "uid":44242, "over_quota":false, "storagehost":"storage1.example.com", "domain":"example.com"}'
---
:request:
:method: :post
:path: "/auth/mailstore"
:payload:
email: user1@example.com
password: pwduser1
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"false","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
---
:request:
:method: :post
:path: "/mail/update_over_quota"
:payload:
address: user1@example.com
over_quota: true
:response:
:status: 200
:body:
- '{"result":"success"}'
---
:request:
:method: :post
:path: "/mail/mailbox"
:payload:
address: user1@example.com
:response:
:status: 200
:body:
- '{"result":"success","email":"user1@example.com", "uid":44242, "over_quota":true, "storagehost":"storage1.example.com", "domain":"example.com"}'
---
:request:
:method: :post
:path: "/auth/mailstore"
:payload:
email: user1@example.com
password: pwduser1
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"true","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
---
:request:
:method: :post
:path: "/mail/update_over_quota"
:payload:
address: user1@example.com
over_quota: mismatch
:response:
:status: 200
:body:
- '{"result":"success"}'
---
:request:
:method: :post
:path: "/mail/mailbox"
:payload:
address: user1@example.com
:response:
:status: 200
:body:
- '{"result":"success","email":"user1@example.com", "uid":44242, "over_quota":false, "storagehost":"storage1.example.com", "domain":"example.com"}'
---
:request:
:method: :post
:path: "/auth/mailstore"
:payload:
email: user1@example.com
password: pwduser1
:response:
:status: 200
:body:
- '{"result":"success","login":{"USER":"user1@example.com","HOME":"/var/mail/mails/user1@example.com","userdb_quota_rule":"*:storage=1G","userdb_quota_over_flag":"false","userdb_uid":"44242","userdb_gid":"12","userdb_home":"/var/mail/mails/user1@example.com","userdb_namespace/inbox/prefix":"","EXTRA":"userdb_quota_rule userdb_quota_over_flag userdb_uid userdb_gid userdb_uid userdb_gid userdb_home userdb_namespace/inbox/prefix"}}'
......@@ -370,6 +370,22 @@ exim_mail_routing_storagehost_fatal:
user: 'exim'
query: 'mail_routing:nosuchuser'
expected: 'fail'
## over_quota
exim_mail_over_quota:
host: 'mailrelay'
user: 'exim'
query: 'over_quota:user1@example.com'
expected: 'false'
exim_mail_over_quota_fwd:
host: 'mailrelay'
user: 'exim'
query: 'over_quota:fwd1@example.com'
expected: 'false'
exim_mail_over_quota_wrong:
host: 'mailrelay'
user: 'exim'
query: 'over_quota:nosuchuser@example.com'
expected: 'false'
## mail_forward
exim_mail_routing_mail_forward:
host: 'mailrelay'
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment