maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package imapsql
 20
 21import (
 22	"context"
 23	"runtime/trace"
 24
 25	"github.com/emersion/go-imap"
 26	"github.com/emersion/go-imap/backend"
 27	"github.com/emersion/go-message/textproto"
 28	"github.com/emersion/go-smtp"
 29	imapsql "github.com/foxcpp/go-imap-sql"
 30	"github.com/foxcpp/maddy/framework/buffer"
 31	"github.com/foxcpp/maddy/framework/exterrors"
 32	"github.com/foxcpp/maddy/framework/module"
 33	"github.com/foxcpp/maddy/internal/target"
 34)
 35
 36type addedRcpt struct {
 37	rcptTo string
 38}
 39type delivery struct {
 40	store    *Storage
 41	msgMeta  *module.MsgMetadata
 42	d        imapsql.Delivery
 43	mailFrom string
 44
 45	addedRcpts map[string]addedRcpt
 46}
 47
 48func (d *delivery) String() string {
 49	return d.store.Name() + ":" + d.store.InstanceName()
 50}
 51
 52func userDoesNotExist(actual error) error {
 53	return &exterrors.SMTPError{
 54		Code:         501,
 55		EnhancedCode: exterrors.EnhancedCode{5, 1, 1},
 56		Message:      "User does not exist",
 57		TargetName:   "imapsql",
 58		Err:          actual,
 59	}
 60}
 61
 62func (d *delivery) AddRcpt(ctx context.Context, rcptTo string, _ smtp.RcptOptions) error {
 63	defer trace.StartRegion(ctx, "sql/AddRcpt").End()
 64
 65	accountName, err := d.store.deliveryNormalize(ctx, rcptTo)
 66	if err != nil {
 67		return userDoesNotExist(err)
 68	}
 69
 70	if _, ok := d.addedRcpts[accountName]; ok {
 71		return nil
 72	}
 73
 74	// This header is added to the message only for that recipient.
 75	// go-imap-sql does certain optimizations to store the message
 76	// with small amount of per-recipient data in a efficient way.
 77	userHeader := textproto.Header{}
 78	userHeader.Add("Delivered-To", accountName)
 79
 80	if err := d.d.AddRcpt(accountName, userHeader); err != nil {
 81		if err == imapsql.ErrUserDoesntExists || err == backend.ErrNoSuchMailbox {
 82			return userDoesNotExist(err)
 83		}
 84		if _, ok := err.(imapsql.SerializationError); ok {
 85			return &exterrors.SMTPError{
 86				Code:         453,
 87				EnhancedCode: exterrors.EnhancedCode{4, 3, 2},
 88				Message:      "Internal server error, try again later",
 89				TargetName:   "imapsql",
 90				Err:          err,
 91			}
 92		}
 93		return err
 94	}
 95
 96	d.addedRcpts[accountName] = addedRcpt{
 97		rcptTo: rcptTo,
 98	}
 99	return nil
100}
101
102func (d *delivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {
103	defer trace.StartRegion(ctx, "sql/Body").End()
104
105	if !d.msgMeta.Quarantine && d.store.filters != nil {
106		for rcpt, rcptData := range d.addedRcpts {
107			folder, flags, err := d.store.filters.IMAPFilter(rcpt, rcptData.rcptTo, d.msgMeta, header, body)
108			if err != nil {
109				d.store.Log.Error("IMAPFilter failed", err, "rcpt", rcpt)
110				continue
111			}
112			d.d.UserMailbox(rcpt, folder, flags)
113		}
114	}
115
116	if d.msgMeta.Quarantine {
117		if err := d.d.SpecialMailbox(imap.JunkAttr, d.store.junkMbox); err != nil {
118			if _, ok := err.(imapsql.SerializationError); ok {
119				return &exterrors.SMTPError{
120					Code:         453,
121					EnhancedCode: exterrors.EnhancedCode{4, 3, 2},
122					Message:      "Storage access serialiation problem, try again later",
123					TargetName:   "imapsql",
124					Err:          err,
125				}
126			}
127			return err
128		}
129	}
130
131	header = header.Copy()
132	header.Add("Return-Path", "<"+target.SanitizeForHeader(d.mailFrom)+">")
133	err := d.d.BodyParsed(header, body.Len(), body)
134	if _, ok := err.(imapsql.SerializationError); ok {
135		return &exterrors.SMTPError{
136			Code:         453,
137			EnhancedCode: exterrors.EnhancedCode{4, 3, 2},
138			Message:      "Storage access serialiation problem, try again later",
139			TargetName:   "imapsql",
140			Err:          err,
141		}
142	}
143	return err
144}
145
146func (d *delivery) Abort(ctx context.Context) error {
147	defer trace.StartRegion(ctx, "sql/Abort").End()
148
149	return d.d.Abort()
150}
151
152func (d *delivery) Commit(ctx context.Context) error {
153	defer trace.StartRegion(ctx, "sql/Commit").End()
154
155	return d.d.Commit()
156}
157
158func (store *Storage) Start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {
159	defer trace.StartRegion(ctx, "sql/Start").End()
160
161	return &delivery{
162		store:      store,
163		msgMeta:    msgMeta,
164		mailFrom:   mailFrom,
165		d:          store.Back.NewDelivery(),
166		addedRcpts: map[string]addedRcpt{},
167	}, nil
168}