Browse Source

Adds support for Private Messaging for users on a pod (#307)

* Add toolbar icon for messages view

* Implement mobile icon for messages (#276)

* Add messages view stub template and handler

* Add single message stub template and handler

* Drop subject

* Add some more stubs

* Add compose reply stub

* Add support for listing multiple messages in messages template

* Implement SendMessageHandler()

* Refactor messaging

* Added integrated SMTP service for private messaging

* Add support for SMTP auth against existing User accounts

* Restrict incoming SMTP messages to on-pod users

* Add POP3 Service with stubs

* List private messages (#305)

* Drop second column from messages view

* Fix message ids and ordering

* Add ViewMessage() support

* Add support for deleting all messages

* Fix tooltip of delete all checkbox

* Add markMessageAsRead()

* Add deleteMessages()

* Add messages cache to keep track of new messages per user

* Add mobile message count badge and desktop new message indicator

* Cleanup

* Fix bug

* Fix bug

Co-authored-by: Mark Wylde <markwylde@users.noreply.github.com>
Co-authored-by: Adrian Emil Grigore <adrian.emil.grigore@gmail.com>
pull/312/head
James Mills 10 months ago
committed by GitHub
parent
commit
6d12f0b7d2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .gitignore
  2. 4
      README.md
  3. 12
      cmd/twtd/main.go
  4. 0
      data/msgs/.gitkeep
  5. 11
      go.mod
  6. 50
      go.sum
  7. 4
      internal/config.go
  8. 3
      internal/context.go
  9. 2
      internal/media_handlers.go
  10. 485
      internal/messages.go
  11. 136
      internal/messages_cache.go
  12. 171
      internal/messages_handler.go
  13. 10
      internal/models.go
  14. 24
      internal/options.go
  15. 184
      internal/rice-box.go
  16. 47
      internal/server.go
  17. 330
      internal/services.go
  18. 60
      internal/static/css/03-icons.css
  19. 15
      internal/static/css/99-twtxt.css
  20. 18
      internal/templates/base.html
  21. 48
      internal/templates/message.html
  22. 61
      internal/templates/messages.html
  23. 18
      internal/templates/settings.html
  24. 7
      internal/utils.go
  25. 2
      types/retwt/retwt.go

4
.gitignore

@ -5,6 +5,8 @@
*.idea
.vscode
*.sw?
*.rej
*.orig
/dist
@ -14,8 +16,10 @@
/cmd/twt/twt
/cmd/twtd/twtd
/data/msgs
/data/cache
/data/blogscache
/data/msgscache
/data/feedsources
/data/settings.yaml

4
README.md

@ -175,10 +175,10 @@ Then visit: http://localhost:8000/
Run twtd:
```console
twtd -r
twtd -R
```
__NOTE:__ Registrations are disabled by default so hence the `-r` flag above.
__NOTE:__ Registrations are disabled by default so hence the `-R` flag above.
Then visit: http://localhost:8000/

12
cmd/twtd/main.go

@ -61,6 +61,10 @@ var (
smtpPass string
smtpFrom string
// Messaging Settings
smtpBind string
pop3Bind string
// Timeouts
sessionExpiry time.Duration
sessionCacheTTL time.Duration
@ -147,6 +151,10 @@ func init() {
flag.StringVar(&smtpPass, "smtp-pass", internal.DefaultSMTPPass, "SMTP Pass to use for email sending")
flag.StringVar(&smtpFrom, "smtp-from", internal.DefaultSMTPFrom, "SMTP From to use for email sending")
// Messaging Settings
flag.StringVar(&smtpBind, "smtp-bind", internal.DefaultSMTPBind, "SMTP interface and port to bind to")
flag.StringVar(&pop3Bind, "pop3-bind", internal.DefaultPOP3Bind, "POP3 interface and port to bind to")
// Timeouts
flag.DurationVar(
&sessionExpiry, "session-expiry", internal.DefaultSessionExpiry,
@ -267,6 +275,10 @@ func main() {
internal.WithSMTPPass(smtpPass),
internal.WithSMTPFrom(smtpFrom),
// Messaging Settings
internal.WithSMTPBind(smtpBind),
internal.WithPOP3Bind(pop3Bind),
// Timeouts
internal.WithSessionExpiry(sessionExpiry),
internal.WithSessionCacheTTL(sessionCacheTTL),

0
data/msgs/.gitkeep

11
go.mod

@ -11,7 +11,6 @@ require (
github.com/PuerkitoBio/goquery v1.5.1
github.com/andreadipersio/securecookie v0.0.0-20131119095127-e3c3b33544ec
github.com/andyleap/microformats v0.0.0-20150523144534-25ae286f528b
github.com/apex/log v1.9.0
github.com/bakape/thumbnailer/v2 v2.6.4
github.com/chai2010/webp v1.1.0
github.com/creasty/defaults v1.5.0
@ -23,6 +22,8 @@ require (
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
github.com/dustin/go-humanize v1.0.0
github.com/elithrar/simple-scrypt v1.3.0
github.com/emersion/go-mbox v1.0.2
github.com/emersion/go-message v0.14.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gabstv/merger v1.0.1
github.com/goccy/go-yaml v1.8.2
@ -37,11 +38,14 @@ require (
github.com/jinzhu/gorm v1.9.15 // indirect
github.com/jinzhu/now v1.1.1 // indirect
github.com/julienschmidt/httprouter v1.3.0
github.com/kr/pretty v0.2.0 // indirect
github.com/lib/pq v1.7.0 // indirect
github.com/lithammer/shortuuid/v3 v3.0.4
github.com/magiconair/properties v1.8.4 // indirect
github.com/marcinwyszynski/popart v0.0.0-20160216095024-f601a19c2970
github.com/marksalpeter/sugar v0.0.0-20160713164314-a69afe358ea8 // indirect
github.com/marksalpeter/token/v2 v2.0.0
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/microcosm-cc/bluemonday v1.0.3
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0
@ -52,12 +56,15 @@ require (
github.com/prologic/bitcask v0.3.9
github.com/prologic/observe v0.0.0-20181231082615-747b185a0928
github.com/prologic/read-file-last-line v0.0.0-20200806014221-326f63458987
github.com/prologic/smtpd v0.0.0-20201215080427-fd3f94c87eb7
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0
github.com/renstrom/shortuuid v3.0.0+incompatible
github.com/rickb777/accept v0.0.0-20170318132422-d5183c44530d
github.com/robfig/cron v1.2.0
github.com/securisec/go-keywords v0.0.0-20200619134240-769e7273f2ed
github.com/sirupsen/logrus v1.7.0
github.com/smartystreets/assertions v1.0.0 // indirect
github.com/spf13/afero v1.4.1 // indirect
github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
@ -76,8 +83,8 @@ require (
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48 // indirect
golang.org/x/text v0.3.4 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.3.0

50
go.sum

@ -49,19 +49,12 @@ github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5z
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andyleap/microformats v0.0.0-20150523144534-25ae286f528b h1:jnCPxFuWTxrUk9L7/0VIFL0mQGFFSwbH0sfQ7XwsTYg=
github.com/andyleap/microformats v0.0.0-20150523144534-25ae286f528b/go.mod h1:I3yyaN+QdpdChOtQg3ApgY01JRmFsXJASweq6Ye5A3s=
github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0=
github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bakape/thumbnailer/v2 v2.6.4 h1:zp3y7k3p355xeQTNyGyBuTc28pYLBuO9n0ZuB9Hk5ms=
@ -114,6 +107,12 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elithrar/simple-scrypt v1.3.0 h1:KIlOlxdoQf9JWKl5lMAJ28SY2URB0XTRDn2TckyzAZg=
github.com/elithrar/simple-scrypt v1.3.0/go.mod h1:U2XQRI95XHY0St410VE3UjT7vuKb1qPwrl/EJwEqnZo=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.14.0 h1:RMEs13hsCJ6I+bsjwD/pq38+bYEj8nMqb/0LUw/PEG8=
github.com/emersion/go-message v0.14.0/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
@ -213,7 +212,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc=
@ -235,9 +233,7 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -269,17 +265,17 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/marcinwyszynski/popart v0.0.0-20160216095024-f601a19c2970 h1:ESBsxVV/MipfxVWDbqKauuBuDdF6/Ed1UDY7lzKmq4I=
github.com/marcinwyszynski/popart v0.0.0-20160216095024-f601a19c2970/go.mod h1:eIKog88Q6dMlbN4UOKyxGsVLlPH4L6HgnH6sXPFPdhI=
github.com/marksalpeter/sugar v0.0.0-20160713164314-a69afe358ea8 h1:1gRDEC07kIwBHulf+5qTzbe44xMMJRck54KK3xl6wYc=
github.com/marksalpeter/sugar v0.0.0-20160713164314-a69afe358ea8/go.mod h1:Pbl6laGVgJOzt66fx/Zxwth5NhgKzyTysQDDqNJxZ3c=
github.com/marksalpeter/token/v2 v2.0.0 h1:kFg3wSxVzozCH3tUFgcAV3Lao8ngz7sdqE+tUUywbPw=
github.com/marksalpeter/token/v2 v2.0.0/go.mod h1:nCWqOuuJXwlt9mi++BPGtRGXPlaItkTZrZGtN4SqhE8=
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
@ -320,8 +316,6 @@ github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4Ns
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
@ -347,6 +341,8 @@ github.com/prologic/observe v0.0.0-20181231082615-747b185a0928 h1:B63MGEQCv0W1lt
github.com/prologic/observe v0.0.0-20181231082615-747b185a0928/go.mod h1:tEdBKdkpsOZCgueJIZwZREodFg5oRhLkTWWNiQ5y84E=
github.com/prologic/read-file-last-line v0.0.0-20200806014221-326f63458987 h1:xPlhozlqV4y0twz46loB/G98pATrMLDz7BMlb0LGUpU=
github.com/prologic/read-file-last-line v0.0.0-20200806014221-326f63458987/go.mod h1:t5ZL5tajhqRldLeY8GpGENXPEl41oaDnpf4LMB5iUM0=
github.com/prologic/smtpd v0.0.0-20201215080427-fd3f94c87eb7 h1:5hhZWF2uhwnvvY/oAKHSJzVzB+ZxuIqIiHPRYF3cnP0=
github.com/prologic/smtpd v0.0.0-20201215080427-fd3f94c87eb7/go.mod h1:GkLsdH1RZj6RDKeI9A05NGZYmEZQ/PbQcZPnZoSZuYI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
@ -365,6 +361,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/renstrom/shortuuid v3.0.0+incompatible h1:F6T1U7bWlI3FTV+JE8HyeR7bkTeYZJntqQLA9ST4HOQ=
github.com/renstrom/shortuuid v3.0.0+incompatible/go.mod h1:n18Ycpn8DijG+h/lLBQVnGKv1BCtTeXo8KKSbBOrQ8c=
github.com/rickb777/accept v0.0.0-20170318132422-d5183c44530d h1:BhTnJzAi1hrLiyTP2//Cb5NMAdaXASdg785m4xRVs/U=
@ -372,14 +370,12 @@ github.com/rickb777/accept v0.0.0-20170318132422-d5183c44530d/go.mod h1:sv64uV+h
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/securisec/go-keywords v0.0.0-20200619134240-769e7273f2ed h1:8ZFy/8C1JaByuTedmMDrLgK7dH/7KPKKXiuJDU0KJYg=
github.com/securisec/go-keywords v0.0.0-20200619134240-769e7273f2ed/go.mod h1:ewJJMApUajQGvQOaQb/QyzTLoL619B5D02XOZlGnlNo=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
@ -387,10 +383,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@ -443,13 +437,6 @@ github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/redcon v1.4.0 h1:y2PmDD55STRdy4S98qP/Dn+gZG+cPVvIDi9BJV2aOwA=
github.com/tidwall/redcon v1.4.0/go.mod h1:IGzxyoKE3Ea5AWIXo/ZHP+hzY8sWXaMKr7KlFgcWSZU=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/unrolled/logger v0.0.0-20190327162521-be1a2406c7c9 h1:EvwTdlXPJXfsN/6dXk+APSGfsGcBdHac6Cd3h7e2fao=
@ -478,7 +465,6 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -518,7 +504,6 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -549,7 +534,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -576,8 +560,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec h1:A1qYjneJuzBZZ2gIB8rd6zrfq6l7SoEMJ8EsSilNK/U=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -636,7 +620,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.30.0 h1:Wk0Z37oBmKj9/n+tPyBHZmeL19LaCoK3Qq48VwYENss=
@ -649,7 +632,6 @@ gopkg.in/ini.v1 v1.53.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
@ -660,8 +642,6 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 h1:XJP7lxbSxWLOMNdBE4B/STaqVy6L73o0knwj2vIlxnw=

4
internal/config.go

@ -53,6 +53,7 @@ type Config struct {
MaxTwtLength int
MaxCacheTTL time.Duration
MaxCacheItems int
MsgsPerPage int
OpenProfiles bool
OpenRegistrations bool
SessionExpiry time.Duration
@ -61,6 +62,9 @@ type Config struct {
MagicLinkSecret string
SMTPBind string
POP3Bind string
SMTPHost string
SMTPPort int
SMTPUser string

3
internal/context.go

@ -64,6 +64,9 @@ type Context struct {
Links types.Links
Alternatives types.Alternatives
Messages Messages
NewMessages int
Twter types.Twter
Twts types.Twts
BlogPost *BlogPost

2
internal/media_handlers.go

@ -9,9 +9,9 @@ import (
"strings"
"time"
"github.com/apex/log"
"github.com/julienschmidt/httprouter"
"github.com/rickb777/accept"
log "github.com/sirupsen/logrus"
)
// MediaHandler ...

485
internal/messages.go

@ -0,0 +1,485 @@
package internal
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/emersion/go-mbox"
"github.com/emersion/go-message"
log "github.com/sirupsen/logrus"
)
const (
msgsDir = "msgs"
rfc2822 = "Mon Jan 02 15:04:05 -0700 2006"
headerKeyTo = "To"
headerKeyDate = "Date"
headerKeyFrom = "From"
headerKeySubject = "Subject"
headerKeyStatus = "Status"
)
type Message struct {
Id int
From string
Sent time.Time
Subject string
Status string
body string
}
func (m Message) User() string {
username, _ := splitEmailAddress(m.From)
return username
}
func (m Message) Text() string {
return m.body
}
type Messages []Message
func (msgs Messages) Len() int {
return len(msgs)
}
func (msgs Messages) Less(i, j int) bool {
return msgs[i].Sent.After(msgs[j].Sent)
}
func (msgs Messages) Swap(i, j int) {
msgs[i], msgs[j] = msgs[j], msgs[i]
}
func deleteAllMessages(conf *Config, username string) error {
path := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(path, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return fmt.Errorf("error creating msgs directory: %w", err)
}
fn := filepath.Join(path, username)
if err := os.Truncate(fn, 0); err != nil {
log.WithError(err).Error("error deleting all messages")
return fmt.Errorf("error deleting all messages: %w", err)
}
return nil
}
func deleteMessages(conf *Config, username string, msgIds []int) error {
path := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(path, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return fmt.Errorf("error creating msgs directory: %w", err)
}
fn := filepath.Join(path, username)
f, err := os.Open(fn)
if err != nil {
log.WithError(err).Error("error opening msgs file")
return fmt.Errorf("error opening msgs file: %w", err)
}
defer f.Close()
of, err := os.OpenFile(fn+".new", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer of.Close()
mr := mbox.NewReader(f)
w := mbox.NewWriter(of)
defer w.Close()
msgIdMap := make(map[int]bool)
for _, msgId := range msgIds {
msgIdMap[msgId] = true
}
id := 1
for {
r, err := mr.NextMessage()
if err == io.EOF {
break
} else if err != nil {
log.WithError(err).Error("error getting next message reader")
return fmt.Errorf("error getting next message reader: %w", err)
}
e, err := message.Read(r)
if err != nil {
log.WithError(err).Error("error reading next message")
return fmt.Errorf("error reading next message: %w", err)
}
if _, ok := msgIdMap[id]; !ok {
from := e.Header.Get(headerKeyFrom)
if from == "" {
return fmt.Errorf("error no `From` header found in message")
}
d, err := time.Parse(rfc2822, e.Header.Get(headerKeyDate))
if err != nil {
log.WithError(err).Error("error parsing message date")
return fmt.Errorf("error parsing message date: %w", err)
}
mw, err := w.CreateMessage(from, d)
if err != nil {
log.WithError(err).Error("error creating message writer")
return fmt.Errorf("error creating message writer: %w", err)
}
if err := e.WriteTo(mw); err != nil {
log.WithError(err).Error("error writing message")
return fmt.Errorf("error writing message: %w", err)
}
}
id++
}
if err := w.Close(); err != nil {
log.WithError(err).Error("error closing message writer")
return fmt.Errorf("error closing message writer: %w", err)
}
if err := of.Close(); err != nil {
log.WithError(err).Error("error closing output file")
return fmt.Errorf("error closing output file: %w", err)
}
if err := f.Close(); err != nil {
log.WithError(err).Error("error closing input file")
return fmt.Errorf("error closing input file: %w", err)
}
if err := os.Rename(of.Name(), fn); err != nil {
log.WithError(err).Error("error renaming message file")
return fmt.Errorf("error renaming message file: %w", err)
}
return nil
}
func markMessageAsRead(conf *Config, username string, msgId int) error {
path := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(path, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return fmt.Errorf("error creating msgs directory: %w", err)
}
fn := filepath.Join(path, username)
f, err := os.Open(fn)
if err != nil {
log.WithError(err).Error("error opening msgs file")
return fmt.Errorf("error opening msgs file: %w", err)
}
defer f.Close()
of, err := os.OpenFile(fn+".new", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer of.Close()
mr := mbox.NewReader(f)
w := mbox.NewWriter(of)
defer w.Close()
id := 1
for {
r, err := mr.NextMessage()
if err == io.EOF {
break
} else if err != nil {
log.WithError(err).Error("error getting next message reader")
return fmt.Errorf("error getting next message reader: %w", err)
}
e, err := message.Read(r)
if err != nil {
log.WithError(err).Error("error reading next message")
return fmt.Errorf("error reading next message: %w", err)
}
if id == msgId {
e.Header.SetText(headerKeyStatus, "RO")
}
from := e.Header.Get(headerKeyFrom)
if from == "" {
return fmt.Errorf("error no `From` header found in message")
}
d, err := time.Parse(rfc2822, e.Header.Get(headerKeyDate))
if err != nil {
log.WithError(err).Error("error parsing message date")
return fmt.Errorf("error parsing message date: %w", err)
}
mw, err := w.CreateMessage(from, d)
if err != nil {
log.WithError(err).Error("error creating message writer")
return fmt.Errorf("error creating message writer: %w", err)
}
if err := e.WriteTo(mw); err != nil {
log.WithError(err).Error("error writing message")
return fmt.Errorf("error writing message: %w", err)
}
id++
}
if err := w.Close(); err != nil {
log.WithError(err).Error("error closing message writer")
return fmt.Errorf("error closing message writer: %w", err)
}
if err := of.Close(); err != nil {
log.WithError(err).Error("error closing output file")
return fmt.Errorf("error closing output file: %w", err)
}
if err := f.Close(); err != nil {
log.WithError(err).Error("error closing input file")
return fmt.Errorf("error closing input file: %w", err)
}
if err := os.Rename(of.Name(), fn); err != nil {
log.WithError(err).Error("error renaming message file")
return fmt.Errorf("error renaming message file: %w", err)
}
return nil
}
func getMessage(conf *Config, username string, msgId int) (msg Message, err error) {
path := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(path, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return msg, fmt.Errorf("error creating msgs directory: %w", err)
}
fn := filepath.Join(path, username)
f, err := os.OpenFile(fn, os.O_CREATE|os.O_RDONLY, 0666)
if err != nil {
log.WithError(err).Error("error opening msgs file")
return msg, fmt.Errorf("error opening msgs file: %w", err)
}
defer f.Close()
mr := mbox.NewReader(f)
id := 1
for {
r, err := mr.NextMessage()
if err == io.EOF {
break
} else if err != nil {
log.WithError(err).Error("error getting next message reader")
return msg, fmt.Errorf("error getting next message reader: %w", err)
}
e, err := message.Read(r)
if err != nil {
log.WithError(err).Error("error reading next message")
return msg, fmt.Errorf("error reading next message: %w", err)
}
if id == msgId {
d, err := time.Parse(rfc2822, e.Header.Get(headerKeyDate))
if err != nil {
log.WithError(err).Error("error parsing message date")
return msg, fmt.Errorf("error parsing message date: %w", err)
}
body, err := ioutil.ReadAll(e.Body)
if err != nil {
log.WithError(err).Error("error reading message body")
return msg, fmt.Errorf("error reading message body: %w", err)
}
return Message{
Id: id,
From: e.Header.Get(headerKeyFrom),
Sent: d,
Subject: e.Header.Get(headerKeySubject),
Status: e.Header.Get(headerKeyStatus),
body: string(body),
}, nil
}
id++
}
return msg, fmt.Errorf("error message not found")
}
func countMessages(conf *Config, username string) (int, error) {
var count int
path := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(path, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return count, fmt.Errorf("error creating msgs directory: %w", err)
}
fn := filepath.Join(path, username)
f, err := os.OpenFile(fn, os.O_CREATE|os.O_RDONLY, 0666)
if err != nil {
log.WithError(err).Error("error opening msgs file")
return count, fmt.Errorf("error opening msgs file: %w", err)
}
defer f.Close()
mr := mbox.NewReader(f)
for {
r, err := mr.NextMessage()
if err == io.EOF {
break
} else if err != nil {
log.WithError(err).Error("error getting next message reader")
return count, fmt.Errorf("error getting next message reader: %w", err)
}
e, err := message.Read(r)
if err != nil {
log.WithError(err).Error("error reading next message")
return count, fmt.Errorf("error reading next message: %w", err)
}
if e.Header.Get(headerKeyStatus) == "" {
count++
}
}
return count, nil
}
func getMessages(conf *Config, username string) (Messages, error) {
var msgs Messages
path := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(path, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return nil, fmt.Errorf("error creating msgs directory: %w", err)
}
fn := filepath.Join(path, username)
f, err := os.OpenFile(fn, os.O_CREATE|os.O_RDONLY, 0666)
if err != nil {
log.WithError(err).Error("error opening msgs file")
return nil, fmt.Errorf("error opening msgs file: %w", err)
}
defer f.Close()
mr := mbox.NewReader(f)
id := 1
for {
r, err := mr.NextMessage()
if err == io.EOF {
break
} else if err != nil {
log.WithError(err).Error("error getting next message reader")
return nil, fmt.Errorf("error getting next message reader: %w", err)
}
e, err := message.Read(r)
if err != nil {
log.WithError(err).Error("error reading next message")
return nil, fmt.Errorf("error reading next message: %w", err)
}
d, err := time.Parse(rfc2822, e.Header.Get(headerKeyDate))
if err != nil {
log.WithError(err).Error("error parsing message date")
return nil, fmt.Errorf("error parsing message date: %w", err)
}
msg := Message{
Id: id,
From: e.Header.Get(headerKeyFrom),
Sent: d,
Subject: e.Header.Get(headerKeySubject),
Status: e.Header.Get(headerKeyStatus),
}
id++
msgs = append(msgs, msg)
}
return msgs, nil
}
func createMessage(from, to, subject string, body io.Reader) (*message.Entity, error) {
var headers message.Header
now := time.Now()
headers.Set(headerKeyFrom, from)
headers.Set(headerKeyTo, to)
headers.Set(headerKeySubject, subject)
headers.Set(headerKeyDate, now.Format(rfc2822))
msg, err := message.New(headers, body)
if err != nil {
log.WithError(err).Error("error creating entity")
return nil, fmt.Errorf("error creating entity: %w", err)
}
return msg, nil
}
func writeMessage(conf *Config, msg *message.Entity, username string) error {
p := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(p, 0755); err != nil {
log.WithError(err).Error("error creating msgs directory")
return err
}
fn := filepath.Join(p, username)
f, err := os.OpenFile(fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return err
}
defer f.Close()
from := msg.Header.Get(headerKeyFrom)
if from == "" {
return fmt.Errorf("error no `From` header found in message")
}
w := mbox.NewWriter(f)
defer w.Close()
mw, err := w.CreateMessage(from, time.Now())
if err != nil {
log.WithError(err).Error("error creating message writer")
return fmt.Errorf("error creating message writer: %w", err)
}
if err := msg.WriteTo(mw); err != nil {
log.WithError(err).Error("error writing message")
return fmt.Errorf("error writing message: %w", err)
}
return nil
}

136
internal/messages_cache.go

@ -0,0 +1,136 @@
package internal
import (
"bytes"
"encoding/gob"
"fmt"
"os"
"path/filepath"
"sync"
log "github.com/sirupsen/logrus"
)
const (
messagesCacheFile = "msgscache"
)
// MessagesCache ...
type MessagesCache struct {
mu sync.RWMutex
MessageCounts map[string]int
}
// NewMessagesCache ...
func NewMessagesCache() *MessagesCache {
return &MessagesCache{
MessageCounts: make(map[string]int),
}
}
// Store ...
func (cache *MessagesCache) Store(path string) error {
b := new(bytes.Buffer)
enc := gob.NewEncoder(b)
err := enc.Encode(cache)
if err != nil {
log.WithError(err).Error("error encoding cache")
return err
}
f, err := os.OpenFile(filepath.Join(path, messagesCacheFile), os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.WithError(err).Error("error opening cache file for writing")
return err
}
defer f.Close()
if _, err = f.Write(b.Bytes()); err != nil {
log.WithError(err).Error("error writing cache file")
return err
}
return nil
}
// LoadMessagesCache ...
func LoadMessagesCache(path string) (*MessagesCache, error) {
cache := &MessagesCache{
MessageCounts: make(map[string]int),
}
f, err := os.Open(filepath.Join(path, messagesCacheFile))
if err != nil {
if !os.IsNotExist(err) {
log.WithError(err).Error("error loading messages cache, cache not found")
return nil, err
}
return cache, nil
}
defer f.Close()
dec := gob.NewDecoder(f)
err = dec.Decode(&cache)
if err != nil {
log.WithError(err).Error("error decoding messages cache")
return nil, err
}
return cache, nil
}
// Refresh ...
func (cache *MessagesCache) Refresh(conf *Config) {
p := filepath.Join(conf.Data, msgsDir)
if err := os.MkdirAll(p, 0755); err != nil {
log.WithError(err).Error("error creating messages directory")
return
}
err := filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.WithError(err).Error("error walking messages directory")
return err
}
count, err := countMessages(conf, info.Name())
if err != nil {
log.WithError(err).Error("error counting messages")
return fmt.Errorf("error counting messages: %w", err)
}
cache.mu.Lock()
cache.MessageCounts[info.Name()] = count
cache.mu.Unlock()
return nil
})
if err != nil {
log.WithError(err).Errorf("error refreshing messages")
return
}
if err := cache.Store(conf.Data); err != nil {
log.WithError(err).Error("error saving blogs cache")
}
}
// Inc ...
func (cache *MessagesCache) Inc(username string) {
cache.mu.Lock()
cache.MessageCounts[username]++
cache.mu.Unlock()
}
// Dec ...
func (cache *MessagesCache) Dec(username string) {
cache.mu.Lock()
cache.MessageCounts[username]--
cache.mu.Unlock()
}
// Get ...
func (cache *MessagesCache) Get(username string) int {
cache.mu.RLock()
count := cache.MessageCounts[username]
cache.mu.RUnlock()
return count
}

171
internal/messages_handler.go

@ -0,0 +1,171 @@
package internal
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/julienschmidt/httprouter"
"github.com/vcraescu/go-paginator"
"github.com/vcraescu/go-paginator/adapter"
)
// ListMessagesHandler ...
func (s *Server) ListMessagesHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
msgs, err := getMessages(s.config, ctx.User.Username)
if err != nil {
ctx.Error = true
ctx.Message = "Error getting messages"
s.render("error", w, ctx)
return
}
sort.Sort(msgs)
var pagedMsgs Messages
page := SafeParseInt(r.FormValue("p"), 1)
pager := paginator.New(adapter.NewSliceAdapter(msgs), s.config.MsgsPerPage)
pager.SetPage(page)
if err := pager.Results(&pagedMsgs); err != nil {
log.WithError(err).Error("error sorting and paging messages")
ctx.Error = true
ctx.Message = "An error occurred while loading messages"
s.render("error", w, ctx)
return
}
ctx.Title = "Private Messages"
ctx.Messages = pagedMsgs
ctx.Pager = &pager
s.render("messages", w, ctx)
return
}
}
// SendMessagesHandler ...
func (s *Server) SendMessageHandler() httprouter.Handle {
localDomain := HostnameFromURL(s.config.BaseURL)
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
from := fmt.Sprintf("%s@%s", ctx.User.Username, localDomain)
recipient := NormalizeUsername(strings.TrimSpace(r.FormValue("recipient")))
if !s.db.HasUser(recipient) {
ctx.Error = true
ctx.Message = "No such user exists!"
s.render("error", w, ctx)
return
}
to := fmt.Sprintf("%s@%s", recipient, localDomain)
subject := strings.TrimSpace(r.FormValue("subject"))
body := strings.NewReader(strings.TrimSpace(r.FormValue("body")))
msg, err := createMessage(from, to, subject, body)
if err != nil {
ctx.Error = true
ctx.Message = "Error creating message"
s.render("error", w, ctx)
return
}
if err := writeMessage(s.config, msg, recipient); err != nil {
ctx.Error = true
ctx.Message = "Error sending message, please try again later!"
s.render("error", w, ctx)
return
}
ctx.Error = false
ctx.Message = "Messages successfully sent"
s.render("error", w, ctx)
return
}
}
// DeleteMessagesHandler ...
func (s *Server) DeleteMessagesHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.FormValue("delete_all") != "" {
if err := deleteAllMessages(s.config, ctx.Username); err != nil {
ctx.Error = true
ctx.Message = "Error deleting all messages! Please try again later"
s.render("error", w, ctx)
return
}
ctx.Error = false
ctx.Message = "All messages successfully deleted!"
s.render("error", w, ctx)
return
}
var msgIds []int
for _, rawId := range r.Form["msgid"] {
id, err := strconv.Atoi(rawId)
if err != nil {
ctx.Error = true
ctx.Message = "Error invalid message id"
s.render("error", w, ctx)
return
}
msgIds = append(msgIds, id)
}
if err := deleteMessages(s.config, ctx.Username, msgIds); err != nil {
ctx.Error = true
ctx.Message = "Error deleting selected messages! Please try again later"
s.render("error", w, ctx)
return
}
ctx.Error = false
ctx.Message = "Selected messages successfully deleted!"
s.render("error", w, ctx)
return
}
}
// ViewMessageHandler ...
func (s *Server) ViewMessageHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
msgId, err := strconv.Atoi(p.ByName("msgid"))
if err != nil {
ctx.Error = true
ctx.Message = "Error invalid message id"
s.render("error", w, ctx)
return
}
msg, err := getMessage(s.config, ctx.Username, msgId)
if err != nil {
ctx.Error = true
ctx.Message = "Error opening message, please try again later!"
s.render("error", w, ctx)
return
}
if err := markMessageAsRead(s.config, ctx.Username, msgId); err != nil {
log.WithError(err).Warnf("error marking message %d for %s as read", msgId, ctx.Username)
}
ctx.Title = fmt.Sprintf("Private Message from %s: %s", msg.From, msg.Subject)
ctx.Messages = Messages{msg}
s.render("message", w, ctx)
return
}
}

10
internal/models.go

@ -56,6 +56,9 @@ type User struct {
Feeds []string `default:"[]"`
Tokens []string `default:"[]"`
SMTPToken string `default:""`
POP3Token string `default:""`
Followers map[string]string `default:"{}"`
Following map[string]string `default:"{}"`
Muted map[string]string `default:"{}"`
@ -232,6 +235,13 @@ func LoadUser(data []byte) (user *User, err error) {
return nil, err
}
if user.SMTPToken == "" {
user.SMTPToken = GenerateRandomToken()
}
if user.POP3Token == "" {
user.POP3Token = GenerateRandomToken()
}
if user.Followers == nil {
user.Followers = make(map[string]string)
}

24
internal/options.go

@ -58,6 +58,9 @@ const (
// of twts in memory
DefaultMaxCacheItems = DefaultTwtsPerPage * 3 // We get bored after paging thorughh > 3 pages :D
// DefaultMsgPerPage is the server's default msgs per page to display
DefaultMsgsPerPage = 20
// DefaultOpenProfiles is the default for whether or not to have open user profiles
DefaultOpenProfiles = false
@ -76,6 +79,10 @@ const (
// DefaultMagicLinkSecret is the jwt magic link secret
DefaultMagicLinkSecret = "PLEASE_CHANGE_ME!!!"
// Default Messaging settings
DefaultSMTPBind = "0.0.0.0:8025"
DefaultPOP3Bind = "0.0.0.0:8110"
// Default SMTP configuration
DefaultSMTPHost = "smtp.gmail.com"
DefaultSMTPPort = 587
@ -137,6 +144,7 @@ func NewConfig() *Config {
TwtPrompts: DefaultTwtPrompts,
TwtsPerPage: DefaultTwtsPerPage,
MaxTwtLength: DefaultMaxTwtLength,
MsgsPerPage: DefaultMsgsPerPage,
OpenProfiles: DefaultOpenProfiles,
OpenRegistrations: DefaultOpenRegistrations,
SessionExpiry: DefaultSessionExpiry,
@ -340,6 +348,22 @@ func WithMagicLinkSecret(secret string) Option {
}
}
// WithSMTPBind sets the interface and port to bind to for SMTP
func WithSMTPBind(smtpBind string) Option {
return func(cfg *Config) error {
cfg.SMTPBind = smtpBind
return nil
}
}
// WithPOP3Bind sets the interface and port to use for POP3
func WithPOP3Bind(pop3Bind string) Option {
return func(cfg *Config) error {
cfg.POP3Bind = pop3Bind
return nil
}
}
// WithSMTPHost sets the SMTPHost to use for sending email
func WithSMTPHost(host string) Option {
return func(cfg *Config) error {

184
internal/rice-box.go

File diff suppressed because one or more lines are too long

47
internal/server.go

@ -50,6 +50,9 @@ type Server struct {
// Blogs Cache
blogs *BlogsCache
// Messages Cache
msgs *MessagesCache
// Feed Cache
cache *Cache
@ -75,11 +78,21 @@ type Server struct {
// API
api *API
// POP3 Service
pop3Service *POP3Service
// SMTP Service
smtpService *SMTPService
// Passwords
pm passwords.Passwords
}
func (s *Server) render(name string, w http.ResponseWriter, ctx *Context) {
if ctx.Authenticated && ctx.Username != "" {
ctx.NewMessages = s.msgs.Get(ctx.User.Username)
}
buf, err := s.templates.Exec(name, ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -106,6 +119,7 @@ func (s *Server) AddShutdownHook(f func()) {
func (s *Server) Shutdown(ctx context.Context) error {
s.cron.Stop()
s.tasks.Stop()
s.smtpService.Stop()
if err := s.server.Shutdown(ctx); err != nil {
log.WithError(err).Error("error shutting down server")
@ -471,6 +485,12 @@ func (s *Server) initRoutes() {
s.router.PATCH("/post", s.am.MustAuth(s.PostHandler()))
s.router.DELETE("/post", s.am.MustAuth(s.PostHandler()))
// Private Messages
s.router.GET("/messages", s.am.MustAuth(s.ListMessagesHandler()))
s.router.GET("/messages/:msgid", s.am.MustAuth(s.ViewMessageHandler()))
s.router.POST("/messages/send", s.am.MustAuth(s.SendMessageHandler()))
s.router.POST("/messages/delete", s.am.MustAuth(s.DeleteMessagesHandler()))
s.router.POST("/blog", s.am.MustAuth(s.PublishBlogHandler()))
s.router.GET("/blogs/:author", s.BlogsHandler())
s.router.GET("/blog/:author/:year/:month/:date/:slug", s.BlogHandler())
@ -625,6 +645,14 @@ func NewServer(bind string, options ...Option) (*Server, error) {
blogs.UpdateBlogs(config)
}
msgs, err := LoadMessagesCache(config.Data)
if err != nil {
log.WithError(err).Error("error loading messages cache (re-creating)")
msgs = NewMessagesCache()
}
log.Info("updating messages cache")
msgs.Refresh(config)
cache, err := LoadCache(config.Data)
if err != nil {
log.WithError(err).Error("error loading feed cache")
@ -676,6 +704,10 @@ func NewServer(bind string, options ...Option) (*Server, error) {
api := NewAPI(router, config, cache, archive, db, pm, tasks)
pop3Service := NewPOP3Service(config, db, pm, tasks)
smtpService := NewSMTPService(config, db, pm, tasks)
server := &Server{
bind: bind,
config: config,
@ -697,9 +729,18 @@ func NewServer(bind string, options ...Option) (*Server, error) {
// API
api: api,
// POP3 Servicee
pop3Service: pop3Service,
// SMTP Servicee
smtpService: smtpService,
// Blogs Cache
blogs: blogs,
// Messages Cache
msgs: msgs,
// Feed Cache
cache: cache,
@ -736,6 +777,12 @@ func NewServer(bind string, options ...Option) (*Server, error) {
server.tasks.Start()
log.Info("started task dispatcher")
server.pop3Service.Start()
log.Info("started POP3 service")
server.smtpService.Start()
log.Info("started SMTP service")
server.setupWebMentions()
log.Infof("started webmentions processor")

330
internal/services.go

@ -0,0 +1,330 @@
package internal
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"encoding/hex"
"fmt"
"hash"
"io"
"io/ioutil"
"net"
"net/mail"
"path/filepath"
"strings"
"time"
"github.com/emersion/go-message"
"github.com/jointwt/twtxt/internal/passwords"
"github.com/marcinwyszynski/popart"
"github.com/prologic/smtpd"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
"github.com/jointwt/twtxt"
)
func parseAddresses(addrs []string) ([]*mail.Address, error) {
var addresses []*mail.Address
for _, addr := range addrs {
address, err := mail.ParseAddress(addr)
if err != nil {
log.WithError(err).Error("error parsing address")
return nil, fmt.Errorf("error parsing address %s: %w", addr, err)
}
addresses = append(addresses, address)
}
return addresses, nil
}
func storeMessage(conf *Config, msg *message.Entity, to []string) error {
addresses, err := parseAddresses(to)
if err != nil {
log.WithError(err).Error("error parsing `To` address list")
return fmt.Errorf("error parsing `To` address list: %w", err)
}
for _, address := range addresses {
username, _ := splitEmailAddress(address.Address)
if err := writeMessage(conf, msg, username); err != nil {
log.WithError(err).Errorf("error writing message for %s", username)
return fmt.Errorf("error writing message for %s: %w", username, err)
}
}
return nil
}
func splitEmailAddress(email string) (string, string) {
components := strings.Split(email, "@")
username, domain := components[0], components[1]
return username, domain
}
func validMAC(fn func() hash.Hash, message, messageMAC, key []byte) bool {
mac := hmac.New(fn, key)
mac.Write(message)
expectedMAC := mac.Sum(nil)
return hmac.Equal(messageMAC, expectedMAC)
}
type mboxHandler struct {
config *Config
db Store
pm passwords.Passwords
tasks *Dispatcher
mboxFile string
username string
}
func NewMboxHandler(config *Config, db Store, pm passwords.Passwords, tasks *Dispatcher) popart.Handler {
return &mboxHandler{config, db, pm, tasks, "", ""}
}
func (m *mboxHandler) AuthenticatePASS(username, password string) error {
if !m.db.HasUser(username) {
return fmt.Errorf("error: invalid credentials")
}
user, err := m.db.GetUser(username)
if err != nil {
log.WithError(err).Error("error loading user object")
return fmt.Errorf("error loading user object: %w", err)