@@ -8,10 +8,17 @@ import ( | |||
const pluginDir = "plugins" | |||
// Plugin extends koushin with additional functionality. | |||
type Plugin interface { | |||
// Name should return the plugin name. | |||
Name() string | |||
// LoadTemplate populates t with the plugin's functions and templates. | |||
LoadTemplate(t *template.Template) error | |||
// SetRoutes populates group with the plugin's routes. | |||
SetRoutes(group *echo.Group) | |||
// Inject is called prior to rendering a template. It can extend the | |||
// template data by setting new items in the Extra map. | |||
Inject(name string, data interface{}) error | |||
// Close is called when the plugin is unloaded. | |||
Close() error | |||
} |
@@ -14,9 +14,10 @@ const cookieName = "koushin_session" | |||
const messagesPerPage = 50 | |||
// Server holds all the koushin server state. | |||
type Server struct { | |||
Sessions *SessionManager | |||
Plugins []Plugin | |||
Plugins []Plugin | |||
imap struct { | |||
host string | |||
@@ -98,11 +99,13 @@ func newServer(imapURL, smtpURL string) (*Server, error) { | |||
type Context struct { | |||
echo.Context | |||
Server *Server | |||
Session *Session | |||
Session *Session // nil if user isn't logged in | |||
} | |||
var aLongTimeAgo = time.Unix(233431200, 0) | |||
// SetSession sets a cookie for the provided session. Passing a nil session | |||
// unsets the cookie. | |||
func (ctx *Context) SetSession(s *Session) { | |||
cookie := http.Cookie{ | |||
Name: cookieName, | |||
@@ -127,6 +130,7 @@ type Options struct { | |||
Theme string | |||
} | |||
// New creates a new server. | |||
func New(e *echo.Echo, options *Options) error { | |||
s, err := newServer(options.IMAPURL, options.SMTPURL) | |||
if err != nil { | |||
@@ -23,8 +23,9 @@ func generateToken() (string, error) { | |||
return base64.URLEncoding.EncodeToString(b), nil | |||
} | |||
var ErrSessionExpired = errors.New("session expired") | |||
var errSessionExpired = errors.New("session expired") | |||
// AuthError wraps an authentication error. | |||
type AuthError struct { | |||
cause error | |||
} | |||
@@ -33,6 +34,7 @@ func (err AuthError) Error() string { | |||
return fmt.Sprintf("authentication failed: %v", err.cause) | |||
} | |||
// Session is an active user session. It may also hold an IMAP connection. | |||
type Session struct { | |||
manager *SessionManager | |||
username, password string | |||
@@ -49,6 +51,8 @@ func (s *Session) ping() { | |||
s.pings <- struct{}{} | |||
} | |||
// Do executes an IMAP operation on this session. The IMAP client can only be | |||
// used from inside f. | |||
func (s *Session) Do(f func(*imapclient.Client) error) error { | |||
s.locker.Lock() | |||
defer s.locker.Unlock() | |||
@@ -65,6 +69,7 @@ func (s *Session) Do(f func(*imapclient.Client) error) error { | |||
return f(s.imapConn) | |||
} | |||
// Close destroys the session. This can be used to log the user out. | |||
func (s *Session) Close() { | |||
select { | |||
case <-s.closed: | |||
@@ -74,6 +79,8 @@ func (s *Session) Close() { | |||
} | |||
} | |||
// SessionManager keeps track of active sessions. It connects and re-connects | |||
// to the upstream IMAP server as necessary. It prunes expired sessions. | |||
type SessionManager struct { | |||
newIMAPClient func() (*imapclient.Client, error) | |||
@@ -113,6 +120,8 @@ func (sm *SessionManager) get(token string) (*Session, error) { | |||
return session, nil | |||
} | |||
// Put connects to the IMAP server and creates a new session. If authentication | |||
// fails, the error will be of type AuthError. | |||
func (sm *SessionManager) Put(username, password string) (*Session, error) { | |||
c, err := sm.connect(username, password) | |||
if err != nil { | |||
@@ -21,6 +21,7 @@ type GlobalRenderData struct { | |||
Username string | |||
// TODO: list of mailboxes | |||
// Additional plugin-specific data | |||
Extra map[string]interface{} | |||
} | |||
@@ -28,7 +29,8 @@ type GlobalRenderData struct { | |||
// template-specific fields. | |||
type RenderData struct { | |||
Global GlobalRenderData | |||
Extra map[string]interface{} | |||
// Additional plugin-specific data | |||
Extra map[string]interface{} | |||
} | |||
func NewRenderData(ctx *Context) *RenderData { | |||