Skip to content

Commit 69828cf

Browse files
committed
Initial commit
0 parents  commit 69828cf

File tree

5 files changed

+307
-0
lines changed

5 files changed

+307
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.txz
2+
postforward
3+
usr/

LICENSE.txt

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright 2017 Nick Groenen <[email protected]>
2+
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are met:
5+
6+
1. Redistributions of source code must retain the above copyright notice, this
7+
list of conditions and the following disclaimer.
8+
9+
2. Redistributions in binary form must reproduce the above copyright notice,
10+
this list of conditions and the following disclaimer in the documentation
11+
and/or other materials provided with the distribution.
12+
13+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Makefile

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.PHONY: build clean freebsd
2+
3+
VERSION := 1.0.0
4+
BUILDNUMBER := 1
5+
6+
# fpm settings
7+
NAME := postforward
8+
ARCH := x86_64
9+
MAINTAINER := Nick Groenen <[email protected]>
10+
DESCRIPTION := Postfix SRS forwarding agent
11+
EXTRA_ARGS :=
12+
13+
14+
.PHONY: build
15+
build:
16+
go build -ldflags="-s -w" *.go
17+
18+
.PHONY: freebsd
19+
freebsd:
20+
mkdir -p usr/local/bin
21+
GOOS=freebsd go build -ldflags="-s -w" -o usr/local/bin/postforward *.go
22+
23+
fpm -f -t freebsd -s dir \
24+
--name "$(NAME)" \
25+
--version "$(VERSION)-$(BUILDNUMBER)" \
26+
--architecture "$(ARCH)" \
27+
--maintainer "$(MAINTAINER)" \
28+
--description "$(DESCRIPTION)" \
29+
$(EXTRA_ARGS) \
30+
usr/
31+
32+
.PHONY: clean
33+
clean:
34+
rm -rf postforward usr/

README.md

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
Postforward
2+
===========
3+
4+
*Postfix SRS forwarding agent.*
5+
6+
7+
About
8+
-----
9+
10+
Postforward is a mail forwarding utility which aims to compliment the
11+
[Postfix Sender Rewriting Scheme daemon (PostSRSd)](https://github.com/roehling/postsrsd).
12+
13+
The downside of using PostSRSd is that all mail is naively rewritten, even
14+
when no forwarding is actually performed. Such rewritten Return-Path
15+
addresses may confuse sieve scripts and other mail filtering software.
16+
17+
This is where Postforward comes in. Instead of rewriting all incoming mail
18+
regardless of final destination, mail systems may be configured to pipe
19+
mail into Postforward only when forwarding needs to happen, leaving
20+
non-forwarded mail unaltered by PostSRSd. Postforward will rewrite
21+
envelope addresses for piped mail using PostSRSd itself and re-inject
22+
these messages back into the queue, destined for the forwarding
23+
recipient(s).
24+
25+
26+
Project status
27+
--------------
28+
29+
This software is actively maintained but considered feature-complete. No
30+
changes or new features are planned except as required to fix any
31+
potential issues that may come up in the future.
32+
33+
34+
Installation
35+
------------
36+
37+
I no longer provide pre-compiled binaries for small-time projects of mine
38+
so you will have to build from sources yourself. If you have an up-to-date
39+
[Go toolchain](https://golang.org/dl/) installed on your system this is as
40+
simple as:
41+
42+
```sh
43+
go get -d github.com/zoni/postforward
44+
cd ~/go/src/github.com/zoni/postforward
45+
make
46+
```
47+
48+
49+
Configuration
50+
-------------
51+
52+
Postforward relies on mail being delivered via stdin so this implies
53+
delivery using Postfix's `local(8)` or `pipe(8)` delivery agents. One such
54+
method may be achieved by configuring a pipe forward in `/etc/aliases`:
55+
56+
```
57+
forwarder: "|/usr/local/bin/postforward [email protected]"
58+
```
59+
60+
*(Note: when running PostSRSd on a different host or port, use the
61+
`--srs-addr` flag to set the correct address here.)*
62+
63+
In `main.cf`, configure `recipient_canonical_maps` and
64+
`recipient_canonical_classes` as
65+
[recommended by PostSRSd](https://github.com/roehling/postsrsd#configuration)
66+
but *do not* set `sender_canonical_maps` or `sender_canonical_classes`.
67+
68+
-----------------------------------------------------------------------------
69+
70+
Note that in case of process errors, postfix bounces emails with the full
71+
process argument string in the DSN message which could leak internal
72+
information such as the forwarding address. This is default postfix
73+
behavior for the local and pipe delivery agents.
74+
75+
If this is undesirable,
76+
[local_delivery_status_filter](http://www.postfix.org/postconf.5.html#local_delivery_status_filter)
77+
may be configured with a PCRE map such as the following to hide this
78+
information (omit the `$2` in the final entry to also strip command
79+
output):
80+
81+
```
82+
/^(2\S+ deliver(s|ed) to file).+/ $1
83+
/^(2\S+ deliver(s|ed) to command).+/ $1
84+
/^(\S+ Command died with status \d+):.*(\. Command output:.*)/ $1$2
85+
```
86+
87+
88+
Performance
89+
-----------
90+
91+
Using Postforward introduces additional overhead caused by forking of
92+
processes which wouldn't happen with direct use of PostSRSd. Unless you
93+
are forwarding very large volumes of mail this extra overhead is likely
94+
negligible in relation to the total processing cost of a complete email
95+
transaction.
96+
97+
Postforward takes care not to buffer entire messages in memory and is
98+
therefor safe to use on very large emails. Only message headers are
99+
buffered in memory for processing, body content is streamed directly into
100+
sendmail.
101+
102+
103+
License
104+
-------
105+
106+
2-Clause BSD. See [LICENSE.txt](LICENSE.txt) for the full license text.

postforward.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"flag"
7+
"fmt"
8+
"io"
9+
"net/mail"
10+
"net/textproto"
11+
"os"
12+
"os/exec"
13+
"time"
14+
)
15+
16+
// Exit codes as defined in <sysexits.h>
17+
const (
18+
// The input data was incorrect in some way. This
19+
// should only be used for user's data and not system
20+
// files.
21+
ExDataErr = 65
22+
// Temporary failure, indicating something that is not
23+
// really an error. In sendmail, this means that a
24+
// mailer (e.g.) could not create a connection, and
25+
// the request should be reattempted later.
26+
ExTempFail = 75
27+
)
28+
29+
var srsAddr = flag.String("srs-addr", "localhost:10001", "TCP address for SRS lookups")
30+
var rpHeader = flag.String("rp-header", "Return-Path", "header name containing the return-path (MAIL FROM) value")
31+
var sendmailPath = flag.String("sendmail-path", "sendmail", "path to the sendmail binary")
32+
var dryRun = flag.Bool("dry-run", false, "show what would be done, don't actually forward mail")
33+
34+
// lookupTCP performs a TCP table lookup for the specified key against the
35+
// given address.
36+
func lookupTCP(addr, key string) (string, error) {
37+
c, err := textproto.Dial("tcp", addr)
38+
if err != nil {
39+
return "", err
40+
}
41+
42+
id, err := c.Cmd("get " + key)
43+
if err != nil {
44+
return "", err
45+
}
46+
c.StartResponse(id)
47+
defer c.EndResponse(id)
48+
49+
code, msg, err := c.ReadCodeLine(-1)
50+
if err != nil {
51+
return "", err
52+
}
53+
switch code {
54+
case 200:
55+
return msg, nil
56+
case 500:
57+
fmt.Fprintf(os.Stderr, "warning: srs: returncode 500 (%v)\n", msg)
58+
return key, nil
59+
default:
60+
return "", fmt.Errorf("srs: unexpected returncode %d (%v)", code, msg)
61+
}
62+
}
63+
64+
// die writes msg to stderr and aborts the program with the given status code.
65+
func die(msg string, code int) {
66+
fmt.Fprintln(os.Stderr, msg)
67+
os.Exit(code)
68+
}
69+
70+
// headerRewriter wraps the given reader and performs header rewriting on read
71+
// data. Specifically, this strips the "From sender time_stamp" envelope header
72+
// inserted by Postfix and adds supplied headers.
73+
//
74+
// Note that the Return-Path header is left intact. Postfix (specifically,
75+
// the cleanup daemon) will replace this header automatically.
76+
func headerRewriter(in io.Reader, headers []string) io.Reader {
77+
buffer := bytes.Buffer{}
78+
scanner := bufio.NewScanner(in)
79+
linenum := 0
80+
for scanner.Scan() {
81+
linenum++
82+
line := scanner.Bytes()
83+
if linenum == 1 {
84+
for _, header := range headers {
85+
buffer.WriteString(header + "\r\n")
86+
}
87+
88+
if bytes.HasPrefix(line, []byte("From ")) {
89+
continue
90+
}
91+
}
92+
buffer.Write(line)
93+
buffer.Write([]byte("\r\n"))
94+
}
95+
return &buffer
96+
}
97+
98+
func main() {
99+
flag.Parse()
100+
hostname, _ := os.Hostname()
101+
102+
buffer := bytes.Buffer{}
103+
message, err := mail.ReadMessage(io.TeeReader(os.Stdin, &buffer))
104+
if err != nil {
105+
die(fmt.Sprintf("Parse error: %s", err), ExDataErr)
106+
}
107+
108+
returnPath := message.Header.Get(*rpHeader)
109+
if returnPath == "" {
110+
die("Parse error: Missing return-path header in message", ExDataErr)
111+
}
112+
113+
extraHeaders := []string{
114+
fmt.Sprintf("Received: by %s (Postforward); %s",
115+
hostname, time.Now().Format("Mon, 2 Jan 2006 15:04:05 -0700")),
116+
fmt.Sprintf("X-Original-Return-Path: %s", returnPath)}
117+
118+
returnPath = returnPath[1 : len(returnPath)-1] // Remove <> brackets
119+
returnPath, err = lookupTCP(*srsAddr, returnPath)
120+
if err != nil {
121+
die(fmt.Sprintf("SRS lookup error: %s", err), ExTempFail)
122+
}
123+
124+
mailreader := io.MultiReader(headerRewriter(&buffer, extraHeaders), os.Stdin)
125+
args := append([]string{"-f", returnPath}, flag.Args()...)
126+
sendmail := exec.Command(*sendmailPath, args...)
127+
sendmail.Stdin = mailreader
128+
sendmail.Stdout = os.Stdout
129+
sendmail.Stderr = os.Stderr
130+
131+
if *dryRun {
132+
fmt.Printf("Would call sendmail with args: %v\n", args)
133+
fmt.Print("Would pipe the following data into sendmail:\n\n")
134+
io.Copy(os.Stdout, mailreader)
135+
os.Exit(0)
136+
}
137+
138+
if err = sendmail.Run(); err != nil {
139+
die(fmt.Sprintf("Error delivering message to sendmail: %s", err), ExTempFail)
140+
}
141+
142+
}

0 commit comments

Comments
 (0)