├── go.mod ├── .gitignore ├── go.sum ├── main.go ├── README.md └── pkg ├── PopMail.go ├── parseMIMEmail.go └── pop3.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hktalent/gopop3 2 | 3 | go 1.18 4 | 5 | require github.com/emersion/go-message v0.15.0 6 | 7 | require github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | popmails_linux_amd64 7 | popmails_macos_amd64 8 | lists.txt 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | 28 | # 29 | *.out 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= 2 | github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= 3 | github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= 4 | github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= 5 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 6 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hktalent/gopop3/pkg" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | func main() { 14 | fmt.Println("How use?\n./popmails lists.txt xxxUser 50 pop3.xxx.com\n") 15 | // os.Args = []string{"", "lists.txt", "xxx", "50", "mail.xx.com"} 16 | Wg := sync.WaitGroup{} 17 | if data, err := ioutil.ReadFile(os.Args[1]); nil != err { 18 | log.Printf("read %s is error: %v\n", os.Args[1], err) 19 | return 20 | } else { 21 | a := strings.Split(string(data), "\n") 22 | for _, x := range a { 23 | j := strings.Split(x, "\t") 24 | if 2 <= len(j) { 25 | x := pkg.GetPopMail(j[0], j[1], os.Args[4], &Wg) 26 | Wg.Add(1) 27 | go x.PopAllMails() 28 | } 29 | } 30 | } 31 | Wg.Wait() 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tweet](https://img.shields.io/twitter/url/http/Hktalent3135773.svg?style=social)](https://twitter.com/intent/follow?screen_name=Hktalent3135773) [![Follow on Twitter](https://img.shields.io/twitter/follow/Hktalent3135773.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=Hktalent3135773) [![GitHub Followers](https://img.shields.io/github/followers/hktalent.svg?style=social&label=Follow)](https://github.com/hktalent/) 2 | 3 | # go4hacker-mails 4 | 5 | Bulk email collection and retrieval,Assist penetration and redness teams in searching for sensitive information in emails for multiple accounts, such as admin, VPN, and password information. All searches are completed in memory without landing, thus avoiding their own risks 6 | 帮助渗透、红对团队在若干账号的邮件中搜索敏感信息,例如admin、vpn账号及密码信息,所有的搜索在内存中完成,不落地,从而规避自己的风险 7 | 8 | ## Usage 9 | searchText eg: 10 | ``` 11 | password 12 | passwd 13 | admin 14 | vpn 15 | ``` 16 | 50 50 indicates that the output will be searched to 50 bytes after the text is found 17 | 18 | go4hacker-mails_macos_amd64 lists.txt searchText 50 MailServer 19 | eg: 20 | popmails lists.txt password 50 pop3.seclover.com 21 | 22 | #### out results save to file: 23 | SMResults.txt 24 | 25 | #### lists.txt 26 | format: user[tab]pswd 27 | ``` 28 | user1 pswd1 29 | user2 pswd2 30 | user3@xx.com pswd1 31 | username1 pswd1 32 | username2 pswd2 33 | username3 pswd3 34 | ``` 35 | 36 | 37 | ## 💖Star 38 | [![Stargazers over time](https://starchart.cc/hktalent/go4hacker-mails.svg)](https://starchart.cc/hktalent/go4hacker-mails) 39 | 40 | # Donation 41 | | Wechat Pay | AliPay | Paypal | BTC Pay |BCH Pay | 42 | | --- | --- | --- | --- | --- | 43 | |||[paypal](https://www.paypal.me/pwned2019) **miracletalent@gmail.com**||| 44 | 45 | -------------------------------------------------------------------------------- /pkg/PopMail.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "hash/fnv" 7 | "io" 8 | "log" 9 | "mime" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type PopMail struct { 16 | User string 17 | Pswd string 18 | Server string 19 | Wg *sync.WaitGroup 20 | } 21 | 22 | func GetStr(r *io.Reader) string { 23 | buf := new(strings.Builder) 24 | n, err := io.Copy(buf, *r) 25 | if err != nil || 0 == n { 26 | //log.Println("GetStr ", err) 27 | return "" 28 | } 29 | return buf.String() 30 | } 31 | 32 | func hash(s string) uint32 { 33 | h := fnv.New32a() 34 | h.Write([]byte(s)) 35 | return h.Sum32() 36 | } 37 | 38 | var rg01 = regexp.MustCompile(`=\?UTF-8\?B\?(.*?)\?=`) 39 | 40 | func DecodeBase64(s string) string { 41 | if s2, err := base64.StdEncoding.DecodeString(s); nil == err { 42 | return string(s2) 43 | } else { 44 | log.Println(err) 45 | } 46 | return s 47 | } 48 | 49 | func DecodeTitle(s string) string { 50 | a := rg01.FindAllStringSubmatch(s, -1) 51 | var a1 []string 52 | for i, _ := range a { 53 | if 2 == len(a[i]) { 54 | a1 = append(a1, DecodeBase64(a[i][1])) 55 | } 56 | } 57 | //log.Println(a) 58 | return strings.Join(a1, " ") 59 | } 60 | func GetPopMail(u, p, s string, Wg *sync.WaitGroup) *PopMail { 61 | //os.MkdirAll(s, os.ModePerm) 62 | if -1 == strings.Index(u, "@") { 63 | a := strings.Split(s, ".")[1:] 64 | u = u + "@" + strings.Join(a, ".") 65 | } 66 | 67 | return &PopMail{User: u, Pswd: p, Server: s, Wg: Wg} 68 | } 69 | 70 | func (r *PopMail) WriteFile(id int, args ...string) { 71 | s := fmt.Sprintf("%d %s", id, strings.Join(args, "\n")) 72 | szFileName := fmt.Sprintf("%s/%d.txt", r.Server, hash(s)) 73 | SaveFile(szFileName, []byte(s)) 74 | } 75 | func (r *PopMail) PopAllMails() { 76 | defer r.Wg.Done() 77 | p := New(Opt{ 78 | Host: r.Server, 79 | Port: 995, 80 | TLSEnabled: true, 81 | //Port: 110, 82 | //TLSEnabled: false, 83 | }) 84 | 85 | // Create a new connection. POP3 connections are stateful and should end 86 | // with a Quit() once the opreations are done. 87 | c, err := p.NewConn() 88 | if err != nil { 89 | log.Printf("%v", err) 90 | return 91 | } 92 | defer c.Quit() 93 | 94 | // Authenticate. 95 | if err := c.Auth(r.User, r.Pswd); err != nil { 96 | log.Printf("%v", err) 97 | return 98 | } 99 | // Print the total number of messages and their size. 100 | count, size, _ := c.Stat() 101 | fmt.Println("total messages=", count, "size=", size) 102 | 103 | // Pull the list of all message IDs and their sizes. 104 | msgs, _ := c.List(0) 105 | for _, m := range msgs { 106 | fmt.Sprintln("id=", m.ID, "size=", m.Size) 107 | } 108 | 109 | // Pull all messages on the server. Message IDs go from 1 to N. 110 | for id := 1; id <= count; id++ { 111 | m, _ := c.Retr(id) 112 | //s1 := m.Header.Get("subject") 113 | dec := new(mime.WordDecoder) 114 | from, _ := dec.DecodeHeader(m.Header.Get("From")) 115 | to, _ := dec.DecodeHeader(m.Header.Get("To")) 116 | subject, _ := dec.DecodeHeader(m.Header.Get("Subject")) 117 | 118 | mediaType, params, err := mime.ParseMediaType(m.Header.Get("Content-Type")) 119 | if err != nil { 120 | log.Printf("%v", err) 121 | return 122 | } 123 | if strings.HasPrefix(mediaType, "multipart/") { 124 | r.ParsePart(m.Body, params["boundary"], 1, r.Server) 125 | } 126 | // DecodeTitle(s1) 127 | r.WriteFile(id, from, to, subject) 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /pkg/parseMIMEmail.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "mime" 11 | "mime/multipart" 12 | "mime/quotedprintable" 13 | "os" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | var r02 = regexp.MustCompile(`^-+=`) 20 | 21 | func SaveFile(s string, data []byte) { 22 | // lists.txt 20 23 | //if 3 < len(os.Args) { 24 | // ioutil.WriteFile(s, data, 0644) 25 | //} 26 | } 27 | 28 | // BuildFileName builds a file name for a MIME part, using information extracted from 29 | // the part itself, as well as a radix and an index given as parameters. 30 | func BuildFileName(part *multipart.Part, radix string, index int) (filename string) { 31 | // 1st try to get the true file name if there is one in Content-Disposition 32 | filename = part.FileName() 33 | if strings.HasSuffix(filename, "=?") { 34 | filename = DecodeTitle(filename) 35 | } 36 | if len(filename) > 0 { 37 | return 38 | } 39 | // If no defaut filename defined, try to build one of the following format : 40 | // "radix-index.ext" where extension is comuputed from the Content-Type of the part 41 | mediaType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 42 | if err == nil { 43 | mime_type, e := mime.ExtensionsByType(mediaType) 44 | if e == nil { 45 | //if strings.HasSuffix(radix, "=?") { 46 | // radix = DecodeTitle(radix) 47 | //} else { 48 | radix = r02.ReplaceAllString(radix, "") 49 | //} 50 | var s09 string 51 | if 0 == len(mime_type) { 52 | s09 = "" 53 | } else { 54 | s09 = mime_type[len(mime_type)-1] 55 | } 56 | return fmt.Sprintf("%s-%d%s", radix, index, s09) 57 | } 58 | } 59 | return 60 | 61 | } 62 | 63 | func DoFileName(s string) string { 64 | if strings.HasPrefix(s, "=?") { 65 | s = DecodeTitle(s) 66 | } else if strings.HasSuffix(s, "?=") { 67 | s = DecodeTitle("=?UTF-8?B?" + s) 68 | } 69 | return s 70 | } 71 | 72 | // 追加到文件中 73 | func AppendFile(szFile, szOut string) { 74 | f, err := os.OpenFile(szFile, 75 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 76 | if err != nil { 77 | log.Println(err) 78 | return 79 | } 80 | defer f.Close() 81 | if _, err := f.WriteString(szOut + "\n"); err != nil { 82 | log.Println(err) 83 | } 84 | } 85 | 86 | func (r *PopMail) SearchText(data []byte) { 87 | if 2 < len(os.Args) { 88 | s := string(data) 89 | s1 := os.Args[2] 90 | j := strings.Index(s, s1) 91 | if -1 < j { 92 | x01 := 200 93 | if 3 < len(os.Args) { 94 | if m, err := strconv.Atoi(os.Args[3]); nil == err { 95 | x01 = m 96 | } 97 | } 98 | n := j + len(s1) + x01 99 | if n > len(s) { 100 | n = len(s) 101 | } 102 | s001 := "[" + r.User + "] " + s[j:n] + "\n===================\n" 103 | AppendFile("SMResults.txt", s001) 104 | fmt.Printf("found in: %s\n==========\n", s001) 105 | } 106 | } 107 | } 108 | 109 | // WitePart decodes the data of MIME part and writes it to the file filename. 110 | func (r *PopMail) WritePart(part *multipart.Part, filename string, path string) { 111 | // Read the data for this MIME part 112 | part_data, err := ioutil.ReadAll(part) 113 | if err != nil || 0 == len(part_data) { 114 | //log.Println("Error reading MIME part data -", err) 115 | return 116 | } 117 | content_transfer_encoding := strings.ToUpper(part.Header.Get("Content-Transfer-Encoding")) 118 | switch { 119 | case strings.Compare(content_transfer_encoding, "BASE64") == 0: 120 | decoded_content, err := base64.StdEncoding.DecodeString(string(part_data)) 121 | if err != nil { 122 | //log.Println("Error decoding base64 -", err) 123 | } else { 124 | r.SearchText(decoded_content) 125 | SaveFile(path+"/"+DoFileName(filename), decoded_content) 126 | } 127 | 128 | case strings.Compare(content_transfer_encoding, "QUOTED-PRINTABLE") == 0: 129 | decoded_content, err := ioutil.ReadAll(quotedprintable.NewReader(bytes.NewReader(part_data))) 130 | if err != nil { 131 | //log.Println("Error decoding quoted-printable -", err) 132 | } else { 133 | r.SearchText(decoded_content) 134 | SaveFile(path+"/"+DoFileName(filename), decoded_content) 135 | } 136 | default: 137 | r.SearchText(part_data) 138 | SaveFile(path+"/"+DoFileName(filename), part_data) 139 | } 140 | } 141 | 142 | // ParsePart parses the MIME part from mime_data, each part being separated by 143 | // boundary. If one of the part read is itself a multipart MIME part, the 144 | // function calls itself to recursively parse all the parts. The parts read 145 | // are decoded and written to separate files, named uppon their Content-Descrption 146 | // (or boundary if no Content-Description available) with the appropriate 147 | // file extension. Index is incremented at each recursive level and is used in 148 | // building the filename where the part is written, as to ensure all filenames 149 | // are distinct. 150 | func (r *PopMail) ParsePart(mime_data io.Reader, boundary string, index int, path string) { 151 | // Instantiate a new io.Reader dedicated to MIME multipart parsing 152 | // using multipart.NewReader() 153 | reader := multipart.NewReader(mime_data, boundary) 154 | if reader == nil { 155 | return 156 | } 157 | //fmt.Println(strings.Repeat(" ", 2*(index-1)), ">>>>>>>>>>>>> ", boundary) 158 | // Go through each of the MIME part of the message Body with NextPart(), 159 | // and read the content of the MIME part with ioutil.ReadAll() 160 | for { 161 | new_part, err := reader.NextPart() 162 | if err == io.EOF { 163 | break 164 | } 165 | if err != nil { 166 | break 167 | } 168 | //for key, value := range new_part.Header { 169 | // fmt.Printf("%s Key: (%+v) - %d Value: (%#v)\n", strings.Repeat(" ", 2*(index-1)), key, len(value), value) 170 | //} 171 | //fmt.Println(strings.Repeat(" ", 2*(index-1)), "------------") 172 | mediaType, params, err := mime.ParseMediaType(new_part.Header.Get("Content-Type")) 173 | if err == nil && strings.HasPrefix(mediaType, "multipart/") { 174 | r.ParsePart(new_part, params["boundary"], index+1, path) 175 | } else { 176 | filename := BuildFileName(new_part, boundary, 1) 177 | r.WritePart(new_part, filename, path) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /pkg/pop3.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/emersion/go-message" 15 | ) 16 | 17 | // Client implements a Client e-mail client. 18 | type Client struct { 19 | opt Opt 20 | } 21 | 22 | // Conn is a stateful connection with the POP3 server/ 23 | type Conn struct { 24 | conn net.Conn 25 | r *bufio.Reader 26 | w *bufio.Writer 27 | } 28 | 29 | // Opt represents the client configuration. 30 | type Opt struct { 31 | Host string `json:"host"` 32 | Port int `json:"port"` 33 | 34 | // Default is 3 seconds. 35 | DialTimeout time.Duration `json:"dial_timeout"` 36 | 37 | TLSEnabled bool `json:"tls_enabled"` 38 | TLSSkipVerify bool `json:"tls_skip_verify"` 39 | } 40 | 41 | // MessageID contains the ID and size of an individual message. 42 | type MessageID struct { 43 | // ID is the numerical index (non-unique) of the message. 44 | ID int 45 | Size int 46 | 47 | // UID is only present if the response is to the UIDL command. 48 | UID string 49 | } 50 | 51 | var ( 52 | lineBreak = []byte("\r\n") 53 | 54 | respOK = []byte("+OK") // `+OK` without additional info 55 | respOKInfo = []byte("+OK ") // `+OK ` 56 | respErr = []byte("-ERR") // `-ERR` without additional info 57 | respErrInfo = []byte("-ERR ") // `-ERR ` 58 | ) 59 | 60 | // New returns a new client object using an existing connection. 61 | func New(opt Opt) *Client { 62 | if opt.DialTimeout < time.Millisecond { 63 | opt.DialTimeout = time.Second * 3 64 | } 65 | 66 | return &Client{ 67 | opt: opt, 68 | } 69 | } 70 | 71 | // NewConn creates and returns live POP3 server connection. 72 | func (c *Client) NewConn() (*Conn, error) { 73 | var ( 74 | addr = fmt.Sprintf("%s:%d", c.opt.Host, c.opt.Port) 75 | ) 76 | 77 | conn, err := net.DialTimeout("tcp", addr, c.opt.DialTimeout) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | // No TLS. 83 | if c.opt.TLSEnabled { 84 | // Skip TLS host verification. 85 | //tlsCfg := tls.Config{} 86 | tlsCfg := tls.Config{ 87 | Renegotiation: tls.RenegotiateOnceAsClient, 88 | InsecureSkipVerify: true, 89 | MinVersion: tls.VersionTLS10, 90 | } 91 | if c.opt.TLSSkipVerify { 92 | tlsCfg.InsecureSkipVerify = c.opt.TLSSkipVerify 93 | } else { 94 | tlsCfg.ServerName = c.opt.Host // strings.Split(c.opt.Host,":")[0] 95 | } 96 | 97 | conn = tls.Client(conn, &tlsCfg) 98 | } 99 | 100 | pCon := &Conn{ 101 | conn: conn, 102 | r: bufio.NewReader(conn), 103 | w: bufio.NewWriter(conn), 104 | } 105 | 106 | // Verify the connection by reading the welcome +OK greeting. 107 | if _, err := pCon.ReadOne(); err != nil { 108 | return nil, err 109 | } 110 | 111 | return pCon, nil 112 | } 113 | 114 | // Send sends a POP3 command to the server. The given comand is suffixed with "\r\n". 115 | func (c *Conn) Send(b string) error { 116 | if _, err := c.w.WriteString(b + "\r\n"); err != nil { 117 | return err 118 | } 119 | return c.w.Flush() 120 | } 121 | 122 | // Cmd sends a command to the server. POP3 responses are either single line or multi-line. 123 | // The first line always with -ERR in case of an error or +OK in case of a successful operation. 124 | // OK+ is always followed by a response on the same line which is either the actual response data 125 | // in case of single line responses, or a help message followed by multiple lines of actual response 126 | // data in case of multiline responses. 127 | // See https://www.shellhacks.com/retrieve-email-pop3-server-command-line/ for examples. 128 | func (c *Conn) Cmd(cmd string, isMulti bool, args ...interface{}) (*bytes.Buffer, error) { 129 | var cmdLine string 130 | 131 | // Repeat a %v to format each arg. 132 | if len(args) > 0 { 133 | format := " " + strings.TrimRight(strings.Repeat("%v ", len(args)), " ") 134 | 135 | // CMD arg1 argn ...\r\n 136 | cmdLine = fmt.Sprintf(cmd+format, args...) 137 | } else { 138 | cmdLine = cmd 139 | } 140 | 141 | if err := c.Send(cmdLine); err != nil { 142 | return nil, err 143 | } 144 | 145 | // Read the first line of response to get the +OK/-ERR status. 146 | b, err := c.ReadOne() 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | // Single line response. 152 | if !isMulti { 153 | return bytes.NewBuffer(b), err 154 | } 155 | 156 | buf, err := c.ReadAll() 157 | return buf, err 158 | } 159 | 160 | // ReadOne reads a single line response from the conn. 161 | func (c *Conn) ReadOne() ([]byte, error) { 162 | b, _, err := c.r.ReadLine() 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | r, err := parseResp(b) 168 | return r, err 169 | } 170 | 171 | // ReadAll reads all lines from the connection until the POP3 multiline terminator "." is encountered 172 | // and returns a bytes.Buffer of all the read lines. 173 | func (c *Conn) ReadAll() (*bytes.Buffer, error) { 174 | buf := &bytes.Buffer{} 175 | 176 | for { 177 | b, _, err := c.r.ReadLine() 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // "." indicates the end of a multi-line response. 183 | if bytes.Equal(b, []byte(".")) { 184 | break 185 | } 186 | 187 | if _, err := buf.Write(b); err != nil { 188 | return nil, err 189 | } 190 | if _, err := buf.Write(lineBreak); err != nil { 191 | return nil, err 192 | } 193 | } 194 | 195 | return buf, nil 196 | } 197 | 198 | // Auth authenticates the given credentials with the server. 199 | func (c *Conn) Auth(user, password string) error { 200 | if err := c.User(user); err != nil { 201 | return err 202 | } 203 | 204 | if err := c.Pass(password); err != nil { 205 | return err 206 | } 207 | 208 | // Issue a NOOP to force the server to respond to the auth. 209 | // Couresy: github.com/TheCreeper/go-pop3 210 | return c.Noop() 211 | } 212 | 213 | // User sends the username to the server. 214 | func (c *Conn) User(s string) error { 215 | _, err := c.Cmd("USER", false, s) 216 | return err 217 | } 218 | 219 | // Pass sends the password to the server. 220 | func (c *Conn) Pass(s string) error { 221 | _, err := c.Cmd("PASS", false, s) 222 | return err 223 | } 224 | 225 | // Stat returns the number of messages and their total size in bytes in the inbox. 226 | func (c *Conn) Stat() (int, int, error) { 227 | b, err := c.Cmd("STAT", false) 228 | if err != nil { 229 | return 0, 0, err 230 | } 231 | 232 | // count size 233 | f := bytes.Fields(b.Bytes()) 234 | 235 | // Total number of messages. 236 | count, err := strconv.Atoi(string(f[0])) 237 | if err != nil { 238 | return 0, 0, err 239 | } 240 | if count == 0 { 241 | return 0, 0, nil 242 | } 243 | 244 | // Total size of all messages in bytes. 245 | size, err := strconv.Atoi(string(f[1])) 246 | if err != nil { 247 | return 0, 0, err 248 | } 249 | 250 | return count, size, nil 251 | } 252 | 253 | // List returns a list of (message ID, message Size) pairs. 254 | // If the optional msgID > 0, then only that particular message is listed. 255 | // The message IDs are sequential, 1 to N. 256 | func (c *Conn) List(msgID int) ([]MessageID, error) { 257 | var ( 258 | buf *bytes.Buffer 259 | err error 260 | ) 261 | 262 | if msgID <= 0 { 263 | // Multiline response listing all messages. 264 | buf, err = c.Cmd("LIST", true) 265 | } else { 266 | // Single line response listing one message. 267 | buf, err = c.Cmd("LIST", false, msgID) 268 | } 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | var ( 274 | out []MessageID 275 | lines = bytes.Split(buf.Bytes(), lineBreak) 276 | ) 277 | 278 | for _, l := range lines { 279 | // id size 280 | f := bytes.Fields(l) 281 | if len(f) == 0 { 282 | break 283 | } 284 | 285 | id, err := strconv.Atoi(string(f[0])) 286 | if err != nil { 287 | return nil, err 288 | } 289 | 290 | size, err := strconv.Atoi(string(f[1])) 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | out = append(out, MessageID{ID: id, Size: size}) 296 | } 297 | 298 | return out, nil 299 | } 300 | 301 | // Uidl returns a list of (message ID, message UID) pairs. If the optional msgID 302 | // is > 0, then only that particular message is listed. It works like Top() but only works on 303 | // servers that support the UIDL command. Messages size field is not available in the UIDL response. 304 | func (c *Conn) Uidl(msgID int) ([]MessageID, error) { 305 | var ( 306 | buf *bytes.Buffer 307 | err error 308 | ) 309 | 310 | if msgID <= 0 { 311 | // Multiline response listing all messages. 312 | buf, err = c.Cmd("UIDL", true) 313 | } else { 314 | // Single line response listing one message. 315 | buf, err = c.Cmd("UIDL", false, msgID) 316 | } 317 | if err != nil { 318 | return nil, err 319 | } 320 | 321 | var ( 322 | out []MessageID 323 | lines = bytes.Split(buf.Bytes(), lineBreak) 324 | ) 325 | 326 | for _, l := range lines { 327 | // id size 328 | f := bytes.Fields(l) 329 | if len(f) == 0 { 330 | break 331 | } 332 | 333 | id, err := strconv.Atoi(string(f[0])) 334 | if err != nil { 335 | return nil, err 336 | } 337 | 338 | out = append(out, MessageID{ID: id, UID: string(f[1])}) 339 | } 340 | 341 | return out, nil 342 | } 343 | 344 | // Retr downloads a message by the given msgID, parses it and returns it as a 345 | // emersion/go-message.message.Entity object. 346 | func (c *Conn) Retr(msgID int) (*message.Entity, error) { 347 | b, err := c.Cmd("RETR", true, msgID) 348 | if err != nil { 349 | return nil, err 350 | } 351 | 352 | m, err := message.Read(b) 353 | if err != nil { 354 | if !message.IsUnknownCharset(err) { 355 | return nil, err 356 | } 357 | } 358 | 359 | return m, nil 360 | } 361 | 362 | // RetrRaw downloads a message by the given msgID and returns the raw []byte 363 | // of the entire message. 364 | func (c *Conn) RetrRaw(msgID int) (*bytes.Buffer, error) { 365 | b, err := c.Cmd("RETR", true, msgID) 366 | return b, err 367 | } 368 | 369 | // Top retrieves a message by its ID with full headers and numLines lines of the body. 370 | func (c *Conn) Top(msgID int, numLines int) (*message.Entity, error) { 371 | b, err := c.Cmd("TOP", true, msgID, numLines) 372 | if err != nil { 373 | return nil, err 374 | } 375 | 376 | m, err := message.Read(b) 377 | if err != nil { 378 | return nil, err 379 | } 380 | 381 | return m, nil 382 | } 383 | 384 | // Dele deletes one or more messages. The server only executes the 385 | // deletions after a successful Quit(). 386 | func (c *Conn) Dele(msgID ...int) error { 387 | for _, id := range msgID { 388 | _, err := c.Cmd("DELE", false, id) 389 | if err != nil { 390 | return err 391 | } 392 | } 393 | return nil 394 | } 395 | 396 | // Rset clears the messages marked for deletion in the current session. 397 | func (c *Conn) Rset() error { 398 | _, err := c.Cmd("RSET", false) 399 | return err 400 | } 401 | 402 | // Noop issues a do-nothing NOOP command to the server. This is useful for 403 | // prolonging open connections. 404 | func (c *Conn) Noop() error { 405 | _, err := c.Cmd("NOOP", false) 406 | return err 407 | } 408 | 409 | // Quit sends the QUIT command to server and gracefully closes the connection. 410 | // Message deletions (DELE command) are only excuted by the server on a graceful 411 | // quit and close. 412 | func (c *Conn) Quit() error { 413 | if _, err := c.Cmd("QUIT", false); err != nil { 414 | return err 415 | } 416 | return c.conn.Close() 417 | } 418 | 419 | // parseResp checks if the response is an error that starts with `-ERR` 420 | // and returns an error with the message that succeeds the error indicator. 421 | // For success `+OK` messages, it returns the remaining response bytes. 422 | func parseResp(b []byte) ([]byte, error) { 423 | if len(b) == 0 { 424 | return nil, nil 425 | } 426 | 427 | if bytes.Equal(b, respOK) { 428 | return nil, nil 429 | } else if bytes.HasPrefix(b, respOKInfo) { 430 | return bytes.TrimPrefix(b, respOKInfo), nil 431 | } else if bytes.Equal(b, respErr) { 432 | return nil, errors.New("unknown error (no info specified in response)") 433 | } else if bytes.HasPrefix(b, respErrInfo) { 434 | return nil, errors.New(string(bytes.TrimPrefix(b, respErrInfo))) 435 | } else { 436 | return nil, fmt.Errorf("unknown response: %s. Neither -ERR, nor +OK", string(b)) 437 | } 438 | } 439 | --------------------------------------------------------------------------------