initial commit
This commit is contained in:
commit
c31bebc2e5
231
imap/imap.go
Normal file
231
imap/imap.go
Normal file
|
@ -0,0 +1,231 @@
|
|||
package imap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tag = ". "
|
||||
other = "* "
|
||||
maxRead = 11000000
|
||||
timeout = 3000 // ms
|
||||
taggedOK = tag + "OK"
|
||||
otherOK = other + "OK"
|
||||
search = other + "SEARCH"
|
||||
)
|
||||
|
||||
type Error string
|
||||
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
type connection struct {
|
||||
*tls.Conn
|
||||
user string
|
||||
passwd string
|
||||
unreadMails []*mail.Message
|
||||
err []error
|
||||
}
|
||||
|
||||
type stateFunc func(*connection) stateFunc
|
||||
|
||||
func expect(response string, tok string) (string, bool) {
|
||||
lines := strings.Split(response, "\n")
|
||||
for _, l := range lines {
|
||||
line := strings.TrimSpace(l)
|
||||
if strings.HasPrefix(line, tok) {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, tok)), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func parseMail(response string) (*mail.Message, error) {
|
||||
reader := bufio.NewReader(strings.NewReader(response))
|
||||
for {
|
||||
line, _ := reader.ReadBytes('\n')
|
||||
if !strings.HasPrefix(string(line), other) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return mail.ReadMessage(reader)
|
||||
}
|
||||
|
||||
/*-------------- states ----------*/
|
||||
|
||||
func connected(c *connection) stateFunc {
|
||||
_ = c.read("server hello")
|
||||
if c.err != nil {
|
||||
return nil
|
||||
} else {
|
||||
return login
|
||||
}
|
||||
}
|
||||
|
||||
func login(c *connection) stateFunc {
|
||||
c.write(cmd(fmt.Sprintf("login %s %s", c.user, c.passwd)))
|
||||
if c.err != nil {
|
||||
return logout
|
||||
}
|
||||
_ = c.read("login response")
|
||||
if c.err != nil {
|
||||
return logout
|
||||
} else {
|
||||
return selectBox
|
||||
}
|
||||
}
|
||||
|
||||
func selectBox(c *connection) stateFunc {
|
||||
c.write(cmd("select Inbox"))
|
||||
if c.err != nil {
|
||||
return logout
|
||||
}
|
||||
_ = c.read("select response")
|
||||
if c.err != nil {
|
||||
return logout
|
||||
} else {
|
||||
return searchUnseen
|
||||
}
|
||||
}
|
||||
|
||||
func searchUnseen(c *connection) stateFunc {
|
||||
c.write(cmd("uid search unseen"))
|
||||
if c.err != nil {
|
||||
return logout
|
||||
}
|
||||
s := c.read("search response")
|
||||
if c.err != nil {
|
||||
return logout
|
||||
} else {
|
||||
if r, ok := expect(s, search); ok {
|
||||
uids := UIDsFromResponse(r)
|
||||
return func(c *connection) stateFunc { return fetch(c, uids) }
|
||||
} else {
|
||||
return logout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetch(c *connection, ids []int) stateFunc {
|
||||
for _, id := range ids {
|
||||
c.write(cmd(fmt.Sprintf("uid fetch %d body[]", id)))
|
||||
if c.err == nil {
|
||||
s := c.read("uid fetch of " + strconv.Itoa(id))
|
||||
if c.err == nil {
|
||||
mail, err := parseMail(s)
|
||||
if err == nil {
|
||||
c.unreadMails = append(c.unreadMails, mail)
|
||||
} else {
|
||||
c.err = append(c.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return logout
|
||||
}
|
||||
|
||||
func logout(c *connection) stateFunc {
|
||||
c.write(cmd("logout"))
|
||||
c.read("logout response")
|
||||
c.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
/*---------- End of states ----------*/
|
||||
|
||||
func UIDsFromResponse(response string) []int {
|
||||
ids := make([]int, 0)
|
||||
if response == "" {
|
||||
return ids
|
||||
}
|
||||
list := strings.Split(response, " ")
|
||||
for _, id := range list {
|
||||
val, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
log.Println("Could not convert uid '" + id + "'")
|
||||
} else {
|
||||
ids = append(ids, val)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func cmd(s string) string {
|
||||
return tag + s + "\r\n"
|
||||
}
|
||||
|
||||
func (c *connection) write(s string) {
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(timeout * time.Second))
|
||||
bytes := []byte(s)
|
||||
n, err := c.Conn.Write(bytes)
|
||||
var desc string
|
||||
if strings.Contains(s, "login") {
|
||||
// would not want to print login credentials in case of error
|
||||
desc = "login"
|
||||
} else {
|
||||
desc = s
|
||||
}
|
||||
if n != len(bytes) || err != nil {
|
||||
c.err = append(c.err, Error("Could not write "+desc))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) read(desc string) string {
|
||||
buf := make([]byte, 0)
|
||||
reader := bufio.NewReader(c.Conn)
|
||||
var err error
|
||||
var conclusion string
|
||||
if desc == "server hello" {
|
||||
conclusion = other
|
||||
} else {
|
||||
conclusion = tag
|
||||
}
|
||||
for {
|
||||
var line []byte
|
||||
c.SetReadDeadline(time.Now().Add(timeout * time.Millisecond))
|
||||
line, tmpErr := reader.ReadBytes('\n')
|
||||
if tmpErr == nil && len(buf)+len(line) <= maxRead {
|
||||
if l := string(line); strings.HasPrefix(l, conclusion) {
|
||||
if strings.HasPrefix(l, conclusion+"OK") {
|
||||
break
|
||||
} else {
|
||||
buf = nil
|
||||
err = Error("Did not receive OK in " + desc + ", got: " + l)
|
||||
break
|
||||
}
|
||||
}
|
||||
buf = append(buf, line...)
|
||||
} else {
|
||||
errMsg := "Error while reading " + desc
|
||||
if tmpErr != nil {
|
||||
errMsg += "\n" + tmpErr.Error()
|
||||
}
|
||||
err = Error(errMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
s := string(buf)
|
||||
if err != nil {
|
||||
c.err = append(c.err, err)
|
||||
return ""
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func Fetch(c *tls.Conn, user string, passwd string) ([]*mail.Message, []error) {
|
||||
state := connected
|
||||
conn := &connection{Conn: c, user: user, passwd: passwd}
|
||||
for state != nil {
|
||||
state = state(conn)
|
||||
}
|
||||
return conn.unreadMails, conn.err
|
||||
}
|
163
libUnreadMail.go
Normal file
163
libUnreadMail.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package unreadMail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"git.gutmet.org/libUnreadMail/imap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Parameters struct {
|
||||
Server string
|
||||
Port int
|
||||
Cert string
|
||||
User string
|
||||
Passwd string
|
||||
}
|
||||
|
||||
type Error string
|
||||
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
func validateParameters(p *Parameters) []error {
|
||||
var err []error
|
||||
p.Cert = strings.TrimSpace(p.Cert)
|
||||
p.User = strings.TrimSpace(p.User)
|
||||
p.Passwd = strings.TrimSpace(p.Passwd)
|
||||
|
||||
if p.Server == "" {
|
||||
err = append(err, Error("Empty server address"))
|
||||
}
|
||||
if p.Port <= 0 || p.Port > 65535 {
|
||||
err = append(err, Error("Invalid port number: "+strconv.Itoa(p.Port)))
|
||||
}
|
||||
if p.Cert == "" {
|
||||
err = append(err, Error("Empty server cert"))
|
||||
}
|
||||
if p.User == "" {
|
||||
err = append(err, Error("Empty user"))
|
||||
}
|
||||
if p.Passwd == "" {
|
||||
err = append(err, Error("Empty passwd"))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func tlsConnection(p *Parameters) (*tls.Conn, error) {
|
||||
roots := x509.NewCertPool()
|
||||
ok := roots.AppendCertsFromPEM([]byte(p.Cert))
|
||||
if !ok {
|
||||
return nil, Error("Failed to set up root cert")
|
||||
}
|
||||
conf := &tls.Config{RootCAs: roots}
|
||||
c, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", p.Server, p.Port), conf)
|
||||
if err != nil {
|
||||
return nil, Error("Failed to dial")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func normalize(m []byte, encoding string) ([]byte, error) {
|
||||
var s []byte
|
||||
var err error
|
||||
if encoding == "quoted-printable" {
|
||||
s, err = ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(string(m))))
|
||||
} else if encoding == "base64" {
|
||||
s, err = ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, strings.NewReader(string(m))))
|
||||
}
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
err = nil
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func getPlainParts(mail *mail.Message) ([]string, []error) {
|
||||
parts := make([]string, 0)
|
||||
errs := make([]error, 0)
|
||||
mediaType, params, err := mime.ParseMediaType(mail.Header.Get("Content-Type"))
|
||||
if err == nil && strings.HasPrefix(mediaType, "multipart/") {
|
||||
mr := multipart.NewReader(mail.Body, params["boundary"])
|
||||
for {
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if contentType := p.Header.Get("Content-Type"); strings.HasPrefix(contentType, "text/plain") {
|
||||
body, _ := ioutil.ReadAll(p)
|
||||
encoding := p.Header.Get("Content-Transfer-Encoding")
|
||||
if encoding != "" {
|
||||
body, err = normalize(body, encoding)
|
||||
if err != nil {
|
||||
body = nil
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
parts = append(parts, string(body))
|
||||
} else {
|
||||
parts = append(parts, "Non-plain part: "+contentType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not a multipart message
|
||||
if strings.HasPrefix(mediaType, "text/plain") {
|
||||
encoding := mail.Header.Get("Content-Transfer-Encoding")
|
||||
body, _ := ioutil.ReadAll(mail.Body)
|
||||
if encoding != "" {
|
||||
body, err = normalize(body, encoding)
|
||||
if err != nil {
|
||||
body = nil
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
parts = append(parts, string(body))
|
||||
} else if contentName := params["name"]; contentName != "" {
|
||||
parts = append(parts, contentName)
|
||||
}
|
||||
}
|
||||
return parts, errs
|
||||
}
|
||||
|
||||
func Fetch(p *Parameters) ([]*mail.Message, []error) {
|
||||
errs := validateParameters(p)
|
||||
if len(errs) == 0 {
|
||||
conn, err := tlsConnection(p)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
return imap.Fetch(conn, p.User, p.Passwd)
|
||||
}
|
||||
}
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
func FetchPlaintext(p *Parameters) ([]string, []error) {
|
||||
mails, errs := Fetch(p)
|
||||
mailsPlain := make([]string, 0)
|
||||
if len(errs) == 0 {
|
||||
for _, mail := range mails {
|
||||
// check for mime parts
|
||||
parts, pErrs := getPlainParts(mail)
|
||||
errs = append(errs, pErrs...)
|
||||
date := mail.Header.Get("Date")
|
||||
from := mail.Header.Get("From")
|
||||
subject := mail.Header.Get("Subject")
|
||||
m := fmt.Sprintf("Date: %s\nFrom: %s\nSubject: %s\n\n%s", date, from, subject, strings.Join(parts, "\n\n"))
|
||||
mailsPlain = append(mailsPlain, m)
|
||||
}
|
||||
}
|
||||
return mailsPlain, errs
|
||||
}
|
32
unreadMail/unreadMail.go
Normal file
32
unreadMail/unreadMail.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Example program for the unreadMail library
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.gutmet.org/libUnreadMail"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
serverPtr := flag.String("server", "", "server IP")
|
||||
portPtr := flag.Int("port", 993, "server port")
|
||||
certPtr := flag.String("cert", "", "PEM cert of server")
|
||||
userPtr := flag.String("user", "", "user login name")
|
||||
passwdPtr := flag.String("passwd", "", "password for user")
|
||||
|
||||
flag.Parse()
|
||||
par := &unreadMail.Parameters{*serverPtr, *portPtr, *certPtr, *userPtr, *passwdPtr}
|
||||
|
||||
mails, errs := unreadMail.FetchPlaintext(par)
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
}
|
||||
for _, mail := range mails {
|
||||
fmt.Println(mail)
|
||||
fmt.Println("\n=*=*=*=*=*=*=*=*=\n")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user