Skip to content

Commit

Permalink
Public release
Browse files Browse the repository at this point in the history
  • Loading branch information
dzeromsk committed Dec 28, 2019
1 parent 21627f2 commit 4a91ff2
Show file tree
Hide file tree
Showing 7 changed files with 1,554 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
suppressions/
crashers/
c.out
coverage.html
key.pem
cert.pem
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# pop3 (s3-pop3-server) - pop3 server with s3 backend

## Overview

`s3-pop3-server` is a service that takes S3 bucket and presents it as pop3 maildrop. `pop3` is a golang library to build pop3 servers. Both are written in pure go. API documentation for `pop3` can be found in [![GoDoc](https://godoc.org/github.com/dzeromsk/pop3?status.svg)](https://godoc.org/github.com/dzeromsk/pop3).

## Installation

```bash
$ go get -u github.com/dzeromsk/pop3/...
```

This will make the `s3-pop3-server` tool available in `${GOPATH}/bin`, which by default means `~/go/bin`.

## Usage of the binary (s3-pop3-server)

`s3-pop3-server` starts pop3 server on port 995 with s3 bucket used as a storage.

```
Usage of s3-pop3-server:
-addr string
Address to listen to (default ":995")
-bucket string
AWS S3 bucket name (default "emails")
-cert string
TLS Certificate used by server (default "cert.pem")
-key string
TLS Private key used by server (default "key.pem")
-region string
AWS S3 bucket region (default "eu-west-1")
```

## Usage of the library (pop3)

API documentation for `pop3` can be found in [![GoDoc](https://godoc.org/github.com/dzeromsk/pop3?status.svg)](https://godoc.org/github.com/dzeromsk/).

```go
import "github.com/dzeromsk/pop3"
...
err := pop3.ListenAndServeTLS(*address, *cert, *key, &s3auth{
bucket: *bucket,
region: *region,
})
if err != nil {
log.Fatalln(err)
}
```

## Features

- Simple.
- No config files.
- Minimal pop3 server feature set.

## Downsides

- All of the files are served from s3.
- Does not support all pop3 commands.

## Philosophy

Sometimes you just want S3 bucket to be accessible via pop3 protocol. For
example when receiving Email with Amazon SES and storing them in S3. There are
ways to make it work with existing pop3 servers. But you don't need all that.
You want something similar to proxy.
169 changes: 169 additions & 0 deletions cmd/s3-pop3-server/generate_cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

// Generate a self-signed X.509 certificate for a TLS server. Outputs to
// 'cert.pem' and 'key.pem' and will overwrite existing files.

package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"log"
"math/big"
"net"
"os"
"strings"
"time"
)

var (
host = flag.String("host", "", "Comma-separated hostnames and IPs to generate a certificate for")
validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
validFor = flag.Duration("duration", 365*24*time.Hour, "Duration that certificate is valid for")
isCA = flag.Bool("ca", false, "whether this cert should be its own Certificate Authority")
rsaBits = flag.Int("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521")
)

func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}

func pemBlockForKey(priv interface{}) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to marshal ECDSA private key: %v", err)
os.Exit(2)
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
default:
return nil
}
}

func main() {
flag.Parse()

if len(*host) == 0 {
log.Fatalf("Missing required --host parameter")
}

var priv interface{}
var err error
switch *ecdsaCurve {
case "":
priv, err = rsa.GenerateKey(rand.Reader, *rsaBits)
case "P224":
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
case "P256":
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case "P384":
priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
fmt.Fprintf(os.Stderr, "Unrecognized elliptic curve: %q", *ecdsaCurve)
os.Exit(1)
}
if err != nil {
log.Fatalf("failed to generate private key: %s", err)
}

var notBefore time.Time
if len(*validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err)
os.Exit(1)
}
}

notAfter := notBefore.Add(*validFor)

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("failed to generate serial number: %s", err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

hosts := strings.Split(*host, ",")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}

if *isCA {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
log.Fatalf("Failed to create certificate: %s", err)
}

certOut, err := os.Create("cert.pem")
if err != nil {
log.Fatalf("failed to open cert.pem for writing: %s", err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Fatalf("failed to write data to cert.pem: %s", err)
}
if err := certOut.Close(); err != nil {
log.Fatalf("error closing cert.pem: %s", err)
}
log.Print("wrote cert.pem\n")

keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Print("failed to open key.pem for writing:", err)
return
}
if err := pem.Encode(keyOut, pemBlockForKey(priv)); err != nil {
log.Fatalf("failed to write data to key.pem: %s", err)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("error closing key.pem: %s", err)
}
log.Print("wrote key.pem\n")
}
101 changes: 101 additions & 0 deletions cmd/s3-pop3-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"flag"
"io"
"log"

"pop3"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)

var (
address = flag.String("addr", ":995", "Address to listen to")
cert = flag.String("cert", "cert.pem", "TLS Certificate used by server")
key = flag.String("key", "key.pem", "TLS Private key used by server")
region = flag.String("region", "eu-west-1", "AWS S3 bucket region")
bucket = flag.String("bucket", "sns-example-com", "AWS S3 bucket name")
)

func main() {
flag.Parse()

err := pop3.ListenAndServeTLS(*address, *cert, *key, &s3auth{
bucket: *bucket,
region: *region,
})
if err != nil {
log.Fatalln(err)
}
}

type s3auth struct {
bucket string
region string
}

func (a *s3auth) Auth(user, pass string) (pop3.Maildropper, error) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(a.region),
// Credentials: credentials.NewStaticCredentials(user, pass, ""),
})
if err != nil {
return nil, err
}

maildrop := &s3maildrop{
svc: s3.New(sess),
bucket: a.bucket,
}

return maildrop, nil
}

type s3maildrop struct {
svc *s3.S3
bucket string
}

func (m *s3maildrop) List() (messages map[string]int, err error) {
resp, err := m.svc.ListObjectsV2(
&s3.ListObjectsV2Input{
Bucket: aws.String(m.bucket),
},
)
if err != nil {
return nil, err
}

messages = make(map[string]int, len(resp.Contents))
for _, item := range resp.Contents {
messages[*item.Key] = int(*item.Size)
}

return messages, nil
}

func (m *s3maildrop) Get(key string, message io.Writer) (err error) {
resp, err := m.svc.GetObject(
&s3.GetObjectInput{
Bucket: aws.String(m.bucket),
Key: aws.String(key),
},
)
defer resp.Body.Close()

_, err = io.Copy(message, resp.Body)
return err
}

func (m *s3maildrop) Delete(key string) (err error) {
_, err = m.svc.DeleteObject(
&s3.DeleteObjectInput{
Bucket: aws.String(m.bucket),
Key: aws.String(key),
},
)
return err
}
Loading

0 comments on commit 4a91ff2

Please sign in to comment.