security: Add proper authentication, RBAC, and tenant isolation
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
)
|
||||
|
||||
// IMAPClient handles reading emails via IMAP
|
||||
type IMAPClient struct {
|
||||
server string
|
||||
port int
|
||||
username string
|
||||
password string
|
||||
useTLS bool
|
||||
}
|
||||
|
||||
// EmailMessage represents an email in the inbox
|
||||
type EmailMessage struct {
|
||||
UID uint32 `json:"uid"`
|
||||
Subject string `json:"subject"`
|
||||
From string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
Date time.Time `json:"date"`
|
||||
Body string `json:"body"`
|
||||
Preview string `json:"preview"`
|
||||
Read bool `json:"read"`
|
||||
Attachments int `json:"attachments"`
|
||||
}
|
||||
|
||||
// NewIMAPClient creates a new IMAP client
|
||||
func NewIMAPClient(server string, port int, username, password string) *IMAPClient {
|
||||
return &IMAPClient{
|
||||
server: server,
|
||||
port: port,
|
||||
username: username,
|
||||
password: password,
|
||||
useTLS: port == 993,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes connection to IMAP server
|
||||
func (c *IMAPClient) Connect() (*client.Client, error) {
|
||||
addr := fmt.Sprintf("%s:%d", c.server, c.port)
|
||||
|
||||
var cl *client.Client
|
||||
var err error
|
||||
|
||||
if c.useTLS {
|
||||
cl, err = client.DialTLS(addr, &tls.Config{InsecureSkipVerify: true})
|
||||
} else {
|
||||
cl, err = client.Dial(addr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial imap: %w", err)
|
||||
}
|
||||
|
||||
if err := cl.Login(c.username, c.password); err != nil {
|
||||
cl.Logout()
|
||||
return nil, fmt.Errorf("imap login: %w", err)
|
||||
}
|
||||
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
// ListMessages fetches emails from inbox
|
||||
func (c *IMAPClient) ListMessages(limit int) ([]EmailMessage, error) {
|
||||
cl, err := c.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cl.Logout()
|
||||
|
||||
// Select INBOX
|
||||
mbox, err := cl.Select("INBOX", false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select inbox: %w", err)
|
||||
}
|
||||
|
||||
if mbox.Messages == 0 {
|
||||
return []EmailMessage{}, nil
|
||||
}
|
||||
|
||||
// Fetch last N messages
|
||||
from := uint32(1)
|
||||
if mbox.Messages > uint32(limit) {
|
||||
from = mbox.Messages - uint32(limit) + 1
|
||||
}
|
||||
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddRange(from, mbox.Messages)
|
||||
|
||||
messages := make(chan *imap.Message, 10)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
done <- cl.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Text}, messages)
|
||||
}()
|
||||
|
||||
var result []EmailMessage
|
||||
for msg := range messages {
|
||||
email := EmailMessage{
|
||||
UID: msg.Uid,
|
||||
Subject: msg.Envelope.Subject,
|
||||
Date: msg.Envelope.Date,
|
||||
Read: !hasFlag(msg.Flags, imap.RecentFlag),
|
||||
}
|
||||
|
||||
if len(msg.Envelope.From) > 0 {
|
||||
email.From = msg.Envelope.From[0].Address()
|
||||
}
|
||||
|
||||
for _, to := range msg.Envelope.To {
|
||||
email.To = append(email.To, to.Address())
|
||||
}
|
||||
|
||||
// Extract preview from body
|
||||
for _, literal := range msg.Body {
|
||||
if buf := make([]byte, 0); literal != nil {
|
||||
buf = make([]byte, literal.Len())
|
||||
n, _ := literal.Read(buf)
|
||||
if n > 0 {
|
||||
body := string(buf[:n])
|
||||
email.Body = body
|
||||
email.Preview = truncate(stripHTML(body), 200)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, email)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return nil, fmt.Errorf("fetch messages: %w", err)
|
||||
}
|
||||
|
||||
// Reverse to show newest first
|
||||
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetMessage fetches a single email by UID
|
||||
func (c *IMAPClient) GetMessage(uid uint32) (*EmailMessage, error) {
|
||||
cl, err := c.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cl.Logout()
|
||||
|
||||
// Select INBOX
|
||||
if _, err := cl.Select("INBOX", false); err != nil {
|
||||
return nil, fmt.Errorf("select inbox: %w", err)
|
||||
}
|
||||
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(uid)
|
||||
|
||||
messages := make(chan *imap.Message, 1)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
done <- cl.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Text}, messages)
|
||||
}()
|
||||
|
||||
var email *EmailMessage
|
||||
for msg := range messages {
|
||||
email = &EmailMessage{
|
||||
UID: msg.Uid,
|
||||
Subject: msg.Envelope.Subject,
|
||||
Date: msg.Envelope.Date,
|
||||
Read: !hasFlag(msg.Flags, imap.RecentFlag),
|
||||
}
|
||||
|
||||
if len(msg.Envelope.From) > 0 {
|
||||
email.From = msg.Envelope.From[0].Address()
|
||||
}
|
||||
|
||||
for _, to := range msg.Envelope.To {
|
||||
email.To = append(email.To, to.Address())
|
||||
}
|
||||
|
||||
for _, literal := range msg.Body {
|
||||
if literal != nil {
|
||||
buf := make([]byte, literal.Len())
|
||||
n, _ := literal.Read(buf)
|
||||
if n > 0 {
|
||||
body := string(buf[:n])
|
||||
email.Body = body
|
||||
email.Preview = truncate(stripHTML(body), 200)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return nil, fmt.Errorf("fetch message: %w", err)
|
||||
}
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
// MarkAsRead marks an email as read
|
||||
func (c *IMAPClient) MarkAsRead(uid uint32) error {
|
||||
cl, err := c.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Logout()
|
||||
|
||||
if _, err := cl.Select("INBOX", false); err != nil {
|
||||
return fmt.Errorf("select inbox: %w", err)
|
||||
}
|
||||
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(uid)
|
||||
|
||||
item := imap.FormatFlagsOp(imap.AddFlags, true)
|
||||
flags := []interface{}{imap.SeenFlag}
|
||||
|
||||
if err := cl.Store(seqset, item, flags, nil); err != nil {
|
||||
return fmt.Errorf("mark as read: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnreadCount returns number of unread messages
|
||||
func (c *IMAPClient) GetUnreadCount() (int, error) {
|
||||
cl, err := c.Connect()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer cl.Logout()
|
||||
|
||||
mbox, err := cl.Select("INBOX", false)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select inbox: %w", err)
|
||||
}
|
||||
|
||||
return int(mbox.Unseen), nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func hasFlag(flags []string, flag string) bool {
|
||||
for _, f := range flags {
|
||||
if f == flag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stripHTML(html string) string {
|
||||
// Simple HTML stripping
|
||||
result := html
|
||||
result = strings.ReplaceAll(result, "<br>", "\n")
|
||||
result = strings.ReplaceAll(result, "<br/>", "\n")
|
||||
result = strings.ReplaceAll(result, "<p>", "\n")
|
||||
result = strings.ReplaceAll(result, "</p>", "")
|
||||
|
||||
// Remove tags
|
||||
for {
|
||||
start := strings.Index(result, "<")
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
end := strings.Index(result[start:], ">")
|
||||
if end == -1 {
|
||||
break
|
||||
}
|
||||
result = result[:start] + result[start+end+1:]
|
||||
}
|
||||
|
||||
// Decode HTML entities
|
||||
result = strings.ReplaceAll(result, " ", " ")
|
||||
result = strings.ReplaceAll(result, "<", "<")
|
||||
result = strings.ReplaceAll(result, ">", ">")
|
||||
result = strings.ReplaceAll(result, "&", "&")
|
||||
result = strings.ReplaceAll(result, """, "\"")
|
||||
|
||||
return strings.TrimSpace(result)
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
// ParseIMAPURL parses an IMAP URL like imaps://user:pass@server:993
|
||||
func ParseIMAPURL(imapURL string) (*IMAPClient, error) {
|
||||
u, err := url.Parse(imapURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password, _ := u.User.Password()
|
||||
port := 993
|
||||
if u.Port() != "" {
|
||||
p, err := strconv.Atoi(u.Port())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port = p
|
||||
}
|
||||
|
||||
return NewIMAPClient(u.Hostname(), port, u.User.Username(), password), nil
|
||||
}
|
||||
Reference in New Issue
Block a user