diff --git a/.gitignore b/.gitignore index 832f78d..6ccd8e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.db .config.toml .idea +.upload \ No newline at end of file diff --git a/README.md b/README.md index 4dcc529..65b92b6 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ - :heavy_check_mark: Wildmat support - :heavy_check_mark: Database (SQLite) - :heavy_check_mark: Basic article posting -- :construction: Article retrieving -- :construction: Multipart article support -- :x: Transit mode +- :heavy_check_mark: Article retrieving +- :heavy_check_mark: Multipart article support +- :construction: Transit mode - :x: Authentication #### Commands @@ -21,8 +21,8 @@ - :heavy_check_mark: `CAPABILITIES` - :heavy_check_mark: `QUIT` - :construction: Article posting - - :construction: `POST` - - :x: `IHAVE` + - :heavy_check_mark: `POST` + - :construction: `IHAVE` - :heavy_check_mark: Article retrieving - :heavy_check_mark: `ARTICLE` - :heavy_check_mark: `HEAD` diff --git a/internal/backend/sqlite/migrations/002_attachments.sql b/internal/backend/sqlite/migrations/002_attachments.sql new file mode 100644 index 0000000..bc46411 --- /dev/null +++ b/internal/backend/sqlite/migrations/002_attachments.sql @@ -0,0 +1,11 @@ +-- +goose Up + +CREATE TABLE IF NOT EXISTS attachments_articles_mapping ( + article_id INTEGER REFERENCES articles(id), + content_type TEXT NOT NULL, + attachment_id TEXT NOT NULL +); + +-- +goose Down + +DROP TABLE IF EXISTS attachments_articles_mapping; \ No newline at end of file diff --git a/internal/backend/sqlite/sqlite.go b/internal/backend/sqlite/sqlite.go index 4508510..8b88fef 100644 --- a/internal/backend/sqlite/sqlite.go +++ b/internal/backend/sqlite/sqlite.go @@ -124,6 +124,15 @@ func (sb *SQLiteBackend) SaveArticle(a models.Article, groups []string) error { return err } } + + // save attachments into db + for _, v := range a.Attachments { + _, err = sb.db.Exec("INSERT INTO attachments_articles_mapping (article_id, content_type, attachment_id) VALUES (?, ?, ?)", articleID, v.ContentType, v.FileName) + if err != nil { + return err + } + } + return err } @@ -135,6 +144,9 @@ func (sb *SQLiteBackend) GetArticle(messageID string) (models.Article, error) { if err := sb.db.Get(&a.ArticleNumber, "SELECT article_number FROM articles_to_groups WHERE article_id = ?", a.ID); err != nil { return a, err } + if err := sb.db.Select(&a.Attachments, "SELECT content_type, attachment_id FROM attachments_articles_mapping WHERE article_id = ?", a.ID); err != nil { + return a, err + } return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header) } @@ -144,6 +156,9 @@ func (sb *SQLiteBackend) GetArticleByNumber(g *models.Group, num int) (models.Ar return a, err } a.ArticleNumber = num + if err := sb.db.Select(&a.Attachments, "SELECT content_type, attachment_id FROM attachments_articles_mapping WHERE article_id = ?", a.ID); err != nil { + return a, err + } return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header) } diff --git a/internal/config/config.go b/internal/config/config.go index 0c915ea..79e396d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,7 @@ type Config struct { BackendType string `toml:"backend_type"` Domain string `toml:"domain"` SQLite SQLiteBackendConfig `toml:"sqlite"` + UploadPath string `toml:"upload_path"` } type SQLiteBackendConfig struct { diff --git a/internal/models/article.go b/internal/models/article.go index af618d9..0ea1e75 100644 --- a/internal/models/article.go +++ b/internal/models/article.go @@ -17,4 +17,10 @@ type Article struct { Header textproto.MIMEHeader `db:"-"` Envelope *enmime.Envelope `db:"-"` ArticleNumber int `db:"-"` + Attachments []Attachment +} + +type Attachment struct { + ContentType string `db:"content_type"` + FileName string `db:"attachment_id"` } diff --git a/internal/server/handler.go b/internal/server/handler.go index cf31def..b55b8ae 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -12,7 +12,9 @@ import ( "github.com/ChronosX88/yans/internal/utils" "github.com/google/uuid" "github.com/jhillyerd/enmime" + "io/ioutil" "net/mail" + "path" "strconv" "strings" "time" @@ -22,9 +24,10 @@ type Handler struct { handlers map[string]func(s *Session, command string, arguments []string, id uint) error backend backend.StorageBackend serverDomain string + uploadPath string } -func NewHandler(b backend.StorageBackend, serverDomain string) *Handler { +func NewHandler(b backend.StorageBackend, serverDomain, uploadPath string) *Handler { h := &Handler{} h.backend = b h.handlers = map[string]func(s *Session, command string, arguments []string, id uint) error{ @@ -49,6 +52,7 @@ func NewHandler(b backend.StorageBackend, serverDomain string) *Handler { protocol.CommandXover: h.handleOver, } h.serverDomain = serverDomain + h.uploadPath = uploadPath return h } @@ -347,13 +351,29 @@ func (h *Handler) handlePost(s *Session, command string, arguments []string, id return err } } - if !parentMessage.Thread.Valid { - var parentHeader mail.Header - err = json.Unmarshal([]byte(parentMessage.HeaderRaw), &parentHeader) - parentMessageID := parentHeader.Get("Message-ID") - a.Thread = sql.NullString{String: parentMessageID, Valid: true} - } else { - a.Thread = parentMessage.Thread + var parentHeader mail.Header + err = json.Unmarshal([]byte(parentMessage.HeaderRaw), &parentHeader) + parentMessageID := parentHeader.Get("Message-ID") + a.Thread = sql.NullString{String: parentMessageID, Valid: true} + } + + if len(envelope.Attachments) > 0 { + // save attachments + for _, v := range envelope.Attachments { + if v.ContentType != "image/jpeg" && v.ContentType != "image/png" && v.ContentType != "image/gif" { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: "disallowed attachment type"}.String()) + } + ext_ := strings.Split(v.FileName, ".") + ext := ext_[len(ext_)-1] + fileName := uuid.New().String() + "." + ext + err = ioutil.WriteFile(path.Join(h.uploadPath, fileName), v.Content, 0644) + if err != nil { + return err + } + a.Attachments = append(a.Attachments, models.Attachment{ + ContentType: v.ContentType, + FileName: fileName, + }) } } @@ -493,7 +513,10 @@ func (h *Handler) handleArticle(s *Session, command string, arguments []string, builder = builder.Header(k, j) } } - builder = builder.Text([]byte(a.Body)) // FIXME currently only plain text is supported + builder = builder.Text([]byte(a.Body)) + for _, v := range a.Attachments { + builder = builder.AddFileAttachment(path.Join(h.uploadPath, v.FileName)) + } p, err := builder.Build() if err != nil { return err diff --git a/internal/server/nntp_server.go b/internal/server/nntp_server.go index 1959d92..635ae7f 100644 --- a/internal/server/nntp_server.go +++ b/internal/server/nntp_server.go @@ -97,7 +97,7 @@ func (ns *NNTPServer) Start() error { id, _ := uuid.NewUUID() closed := make(chan bool) - session, err := NewSession(ctx, conn, Capabilities, id.String(), closed, NewHandler(ns.backend, ns.cfg.Domain)) + session, err := NewSession(ctx, conn, Capabilities, id.String(), closed, NewHandler(ns.backend, ns.cfg.Domain, ns.cfg.UploadPath)) ns.sessionPoolMutex.Lock() ns.sessionPool[id.String()] = session ns.sessionPoolMutex.Unlock()