commit d7ca591b764eb472b23e12ca611d58967dfe6c14 Author: nahakubuilder Date: Sun Aug 9 18:03:09 2026 +0100 first commit diff --git a/.claude/go-web-app-no-deps-SKILL.md b/.claude/go-web-app-no-deps-SKILL.md new file mode 100644 index 0000000..74902e7 --- /dev/null +++ b/.claude/go-web-app-no-deps-SKILL.md @@ -0,0 +1,255 @@ +--- +name: go-web-app-no-deps +description: Build a server-rendered web application in Go using ONLY the standard library (net/http, html/template, database/sql, embed) — no web framework (no Gin/Echo/Fiber/gorilla), no template engine beyond stdlib html/template, no third-party JS framework. Uses a Flask/Jinja2-style template inheritance pattern (a base.html layout with named blocks that page templates override) and a dark Tailwind CSS theme with a defined CSS custom-property palette. Use this whenever the user asks for a Go web app, admin panel, dashboard, file manager, webmail client, or any browser-based tool built in Go without third-party dependencies, or mentions wanting "Flask-like templates in Go" or a dark Tailwind theme for a Go backend. Also use when debugging a Go html/template project where blocks from different pages are bleeding into each other, or where static assets aren't updating in the browser after a change. +--- + +# Go Web App, No Third-Party Dependencies + +Two real projects converged on this exact pattern: a multi-account webmail +client and a browser-based file explorer, both pure Go + stdlib +`html/template` + vanilla JS + Tailwind CSS dark theme. The patterns below +are what actually worked after hitting real bugs — not a theoretical +"here's how you'd probably do it." + +## Project layout + +``` +project/ +├── cmd/appname/main.go +├── internal/ +│ ├── handlers/ # HTTP handlers, one file per feature area +│ ├── / # the actual app logic, no http.* imports here +│ └── render/ # the template Renderer (see below) +├── web/ +│ ├── templates/ +│ │ ├── base.html # the ONE layout every page extends +│ │ └── .html # one file per page (login.html, app.html, admin.html...) +│ └── static/ +│ ├── css/appname.css +│ └── js/appname.js +└── go.mod +``` + +Embed `web/` into the binary with `//go:embed web/templates web/static` on +a package-level `embed.FS` var — ships as one binary, no separate asset +deploy step. + +## The template renderer — get this right first, it's where the real bug lives + +**The wrong way (looks fine until you have 2+ pages):** parsing every +template file together with `template.ParseGlob("web/templates/*.html")` +into one shared template set. Every page file typically defines a block +named the same thing (`{{define "content"}}` or similar) to plug into the +layout. When they're all parsed into one shared namespace, **the last +one parsed silently wins for every page** — page A renders page B's +content, or a block from one page bleeds into another. This is subtle: +it can look correct for a single page and only breaks once a second page +exists. + +**The right way:** parse the base layout + exactly one page file into a +**fresh template instance per page**, so no other page's `{{define}}` +blocks are ever in the same namespace. + +```go +package render + +import ( + "bytes" + "embed" + "fmt" + "html/template" + "net/http" +) + +type Renderer struct { + templates map[string]*template.Template +} + +// pages lists every page template that extends base.html. +var pages = []string{"login.html", "app.html", "admin.html"} + +func New(fsys embed.FS) (*Renderer, error) { + r := &Renderer{templates: make(map[string]*template.Template, len(pages))} + for _, page := range pages { + // Fresh template.New("base") EACH iteration — this is the whole fix. + t, err := template.New("base").ParseFS(fsys, "web/templates/base.html", "web/templates/"+page) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", page, err) + } + name := page[:len(page)-len(".html")] + r.templates[name] = t + } + return r, nil +} + +// Render executes "base" (the layout), which pulls in this page's block +// overrides — rendered to a buffer first so a mid-execution template error +// never sends a half-written response to the client. +func (r *Renderer) Render(w http.ResponseWriter, status int, name string, data any) { + t, ok := r.templates[name] + if !ok { + http.Error(w, "unknown page: "+name, http.StatusInternalServerError) + return + } + var buf bytes.Buffer + if err := t.ExecuteTemplate(&buf, "base", data); err != nil { + http.Error(w, "render error: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(status) + buf.WriteTo(w) +} +``` + +Construct one `*Renderer` at startup (fails fast on any template syntax +error) and pass it to every handler — don't reparse per-request. + +## base.html — the Flask-`{% block %}` equivalent + +Go's `html/template` has `{{block "name" .}}...default...{{end}}` +(defines a block with a default body, overridable) and `{{define "name"}}` +(overrides it from another file) — this is structurally the same idea as +Jinja2's `{% block %}` / `{% extends %}` + child-template overrides, just +different syntax. + +```html +{{define "base"}} + + + + +{{block "title" .}}App{{end}} + +{{block "head_extra" .}}{{end}} + + +{{block "body" .}}{{end}} + +{{block "scripts" .}}{{end}} + + +{{end}} +``` + +A page file overrides only the blocks it needs: + +```html +{{define "title"}}Sign in{{end}} +{{define "body_class"}}login-page{{end}} +{{define "body"}} + +{{end}} +``` + +`.AssetVersion` (a build timestamp or git short-hash passed into every +`Render` call's data) matters more than it looks — see cache-busting below. + +## Static assets: embed + version query param, always + +```go +staticFS, _ := fs.Sub(webFS, "web/static") +mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) +``` + +Browsers aggressively cache `/static/*` files. During active development +this reliably causes "I edited the CSS/JS and nothing changed" confusion. +Fix it at the template level, not by fighting cache headers: put a +version string in every static asset URL (`app.css?v=`) and bump +it whenever the file changes — a build timestamp baked in at compile time +via `-ldflags "-X main.buildVersion=..."` works well and needs no manual +bumping. For endpoints returning dynamic content that must never be +stale even mid-session (an edited file's raw content, a just-changed +API response), also set explicit headers: `Cache-Control: no-cache` for +GETs that should always revalidate, `Cache-Control: no-store` for +mutating POSTs. + +## Dark Tailwind theme — CSS custom properties, not hardcoded hex everywhere + +Use Tailwind's CDN build (` + + + +
+ + + + + \ No newline at end of file diff --git a/graphify-out/graph.json b/graphify-out/graph.json new file mode 100644 index 0000000..401f5d1 --- /dev/null +++ b/graphify-out/graph.json @@ -0,0 +1,34167 @@ +{ + "directed": false, + "multigraph": false, + "graph": { + "hyperedges": [ + { + "id": "gomail_project_documentation_set", + "label": "GoMail Project Documentation Set", + "nodes": [ + "gomail_handover_overview", + "readme_gomail", + "gomail_action_plan_v4_overview", + "_claude_iterative_build_discipline_skill_gomail_project" + ], + "relation": "participate_in", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "GOMAIL_HANDOVER.md" + }, + { + "id": "admin_portal_crud_feature_set", + "label": "Admin Portal CRUD Feature Set", + "nodes": [ + "internal_admin_static_index_domains_crud", + "internal_admin_static_index_users_crud", + "internal_admin_static_index_rules_crud", + "internal_admin_static_index_queue_crud", + "internal_admin_static_index_quarantine" + ], + "relation": "implement", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "internal/admin/static/index.html" + }, + { + "id": "recurring_bug_pattern_docs", + "label": "Recurring Bug-Pattern Documentation Across GoMail Docs", + "nodes": [ + "_claude_iterative_build_discipline_skill_sqlite_single_conn", + "gomail_handover_sqlite_deadlock_bug", + "gomail_action_plan_v4_sqlite_deadlock_bug", + "_claude_iterative_build_discipline_skill_tcp_read_pattern", + "gomail_handover_tcp_bug", + "gomail_action_plan_v4_tcp_bug" + ], + "relation": "form", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "GOMAIL_HANDOVER.md" + } + ] + }, + "nodes": [ + { + "label": "main.go", + "file_type": "code", + "source_file": "cmd/gomail/main.go", + "source_location": "L1", + "_origin": "ast", + "id": "cmd_gomail_main", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "main.go" + }, + { + "label": "main()", + "file_type": "code", + "source_file": "cmd/gomail/main.go", + "source_location": "L38", + "_origin": "ast", + "id": "cmd_gomail_main_main", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "main()" + }, + { + "label": "buildOAuthConfigs()", + "file_type": "code", + "source_file": "cmd/gomail/main.go", + "source_location": "L332", + "_origin": "ast", + "id": "cmd_gomail_main_buildoauthconfigs", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "buildoauthconfigs()" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "cmd_gomail_main_go_config", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "config" + }, + { + "label": "webmail", + "file_type": "code", + "type": "package", + "ecosystem": "go", + "source_file": "go.mod", + "source_location": "L1", + "_origin": "ast", + "id": "pkg_webmail", + "community": 63, + "community_name": "Go Module Root", + "norm_label": "webmail" + }, + { + "label": "link.go", + "file_type": "code", + "source_file": "internal/accounts/link.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_accounts_link", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "link.go" + }, + { + "label": "LinkIMAPAccount()", + "file_type": "code", + "source_file": "internal/accounts/link.go", + "source_location": "L18", + "_origin": "ast", + "id": "internal_accounts_link_linkimapaccount", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "linkimapaccount()" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_link_go_db", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "db" + }, + { + "label": "wellKnownIMAPHost()", + "file_type": "code", + "source_file": "internal/accounts/link.go", + "source_location": "L58", + "_origin": "ast", + "id": "internal_accounts_link_wellknownimaphost", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "wellknownimaphost()" + }, + { + "label": "LinkOAuth2Account()", + "file_type": "code", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "_origin": "ast", + "id": "internal_accounts_link_linkoauth2account", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "linkoauth2account()" + }, + { + "label": "ProviderFor()", + "file_type": "code", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "_origin": "ast", + "id": "internal_accounts_link_providerfor", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "providerfor()" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_link_go_config", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "config" + }, + { + "label": "provider.go", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_accounts_provider", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "provider.go" + }, + { + "label": "Folder", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L10", + "_origin": "ast", + "id": "accounts_folder", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "folder" + }, + { + "label": "MessageHeader", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L18", + "_origin": "ast", + "id": "accounts_messageheader", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "messageheader" + }, + { + "label": "FullMessage", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L29", + "_origin": "ast", + "id": "accounts_fullmessage", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "fullmessage" + }, + { + "label": "OutgoingMessage", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L34", + "_origin": "ast", + "id": "accounts_outgoingmessage", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "outgoingmessage" + }, + { + "label": "ListOpts", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L43", + "_origin": "ast", + "id": "accounts_listopts", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "listopts" + }, + { + "label": "SyncResult", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L48", + "_origin": "ast", + "id": "accounts_syncresult", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "syncresult" + }, + { + "label": "MailProvider", + "file_type": "code", + "source_file": "internal/accounts/provider.go", + "source_location": "L58", + "_origin": "ast", + "id": "accounts_mailprovider", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "mailprovider" + }, + { + "label": "provider_gomail.go", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_accounts_provider_gomail", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "provider_gomail.go" + }, + { + "label": "GoMailProvider", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L22", + "_origin": "ast", + "id": "accounts_gomailprovider", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "gomailprovider" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_go_db", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "db" + }, + { + "label": "NewGoMailProvider()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L28", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_newgomailprovider", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "newgomailprovider()" + }, + { + "label": ".ListFolders()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L32", + "_origin": "ast", + "id": "accounts_gomailprovider_listfolders", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".listfolders()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_go_context", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "context" + }, + { + "label": ".ListMessages()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L60", + "_origin": "ast", + "id": "accounts_gomailprovider_listmessages", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".listmessages()" + }, + { + "label": ".GetMessage()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L92", + "_origin": "ast", + "id": "accounts_gomailprovider_getmessage", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".getmessage()" + }, + { + "label": ".SendMessage()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L115", + "_origin": "ast", + "id": "accounts_gomailprovider_sendmessage", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".sendmessage()" + }, + { + "label": ".SetFlags()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L149", + "_origin": "ast", + "id": "accounts_gomailprovider_setflags", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".setflags()" + }, + { + "label": ".Move()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L168", + "_origin": "ast", + "id": "accounts_gomailprovider_move", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".move()" + }, + { + "label": ".Delete()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L179", + "_origin": "ast", + "id": "accounts_gomailprovider_delete", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".delete()" + }, + { + "label": ".Sync()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L197", + "_origin": "ast", + "id": "accounts_gomailprovider_sync", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".sync()" + }, + { + "label": "folderType()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L203", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_foldertype", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "foldertype()" + }, + { + "label": "headerFromRaw()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L220", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_headerfromraw", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "headerfromraw()" + }, + { + "label": "domainOf()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L235", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_domainof", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "domainof()" + }, + { + "label": "buildRFC5322()", + "file_type": "code", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L243", + "_origin": "ast", + "id": "internal_accounts_provider_gomail_buildrfc5322", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "buildrfc5322()" + }, + { + "label": "provider_imap.go", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_accounts_provider_imap", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "provider_imap.go" + }, + { + "label": "IMAPCredential", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L23", + "_origin": "ast", + "id": "accounts_imapcredential", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "imapcredential" + }, + { + "label": "OAuth2Credential", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L31", + "_origin": "ast", + "id": "accounts_oauth2credential", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "oauth2credential" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_provider_imap_go_time", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "time" + }, + { + "label": "IMAPProvider", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L42", + "_origin": "ast", + "id": "accounts_imapprovider", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "imapprovider" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_provider_imap_go_db", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "db" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_provider_imap_go_config", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "config" + }, + { + "label": "NewIMAPProvider()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L49", + "_origin": "ast", + "id": "internal_accounts_provider_imap_newimapprovider", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "newimapprovider()" + }, + { + "label": "NewIMAPProviderOAuth2()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "_origin": "ast", + "id": "internal_accounts_provider_imap_newimapprovideroauth2", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "newimapprovideroauth2()" + }, + { + "label": ".connect()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L63", + "_origin": "ast", + "id": "accounts_imapprovider_connect", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".connect()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_accounts_provider_imap_go_context", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "context" + }, + { + "label": "Client", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "client", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "client" + }, + { + "label": ".loginOAuth2()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L112", + "_origin": "ast", + "id": "accounts_imapprovider_loginoauth2", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".loginoauth2()" + }, + { + "label": ".ListFolders()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L152", + "_origin": "ast", + "id": "accounts_imapprovider_listfolders", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".listfolders()" + }, + { + "label": ".ListMessages()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L178", + "_origin": "ast", + "id": "accounts_imapprovider_listmessages", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".listmessages()" + }, + { + "label": ".GetMessage()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L230", + "_origin": "ast", + "id": "accounts_imapprovider_getmessage", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".getmessage()" + }, + { + "label": ".SendMessage()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L264", + "_origin": "ast", + "id": "accounts_imapprovider_sendmessage", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".sendmessage()" + }, + { + "label": ".SetFlags()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L268", + "_origin": "ast", + "id": "accounts_imapprovider_setflags", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".setflags()" + }, + { + "label": ".Move()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L281", + "_origin": "ast", + "id": "accounts_imapprovider_move", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".move()" + }, + { + "label": ".Delete()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L290", + "_origin": "ast", + "id": "accounts_imapprovider_delete", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".delete()" + }, + { + "label": ".Sync()", + "file_type": "code", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L306", + "_origin": "ast", + "id": "accounts_imapprovider_sync", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": ".sync()" + }, + { + "label": "provider_smtp_helper.go", + "file_type": "code", + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_accounts_provider_smtp_helper", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "provider_smtp_helper.go" + }, + { + "label": "sendViaSMTP()", + "file_type": "code", + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L18", + "_origin": "ast", + "id": "internal_accounts_provider_smtp_helper_sendviasmtp", + "community": 4, + "community_name": "Mail Provider Abstraction", + "norm_label": "sendviasmtp()" + }, + { + "label": "challenge.go", + "file_type": "code", + "source_file": "internal/acme/challenge.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_acme_challenge", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "challenge.go" + }, + { + "label": "ChallengeResponder", + "file_type": "code", + "source_file": "internal/acme/challenge.go", + "source_location": "L13", + "_origin": "ast", + "id": "acme_challengeresponder", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "challengeresponder" + }, + { + "label": "RWMutex", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_acme_challenge_go_rwmutex", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "rwmutex" + }, + { + "label": "NewChallengeResponder()", + "file_type": "code", + "source_file": "internal/acme/challenge.go", + "source_location": "L18", + "_origin": "ast", + "id": "internal_acme_challenge_newchallengeresponder", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "newchallengeresponder()" + }, + { + "label": ".Set()", + "file_type": "code", + "source_file": "internal/acme/challenge.go", + "source_location": "L22", + "_origin": "ast", + "id": "acme_challengeresponder_set", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".set()" + }, + { + "label": ".Remove()", + "file_type": "code", + "source_file": "internal/acme/challenge.go", + "source_location": "L28", + "_origin": "ast", + "id": "acme_challengeresponder_remove", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".remove()" + }, + { + "label": ".ServeHTTP()", + "file_type": "code", + "source_file": "internal/acme/challenge.go", + "source_location": "L34", + "_origin": "ast", + "id": "acme_challengeresponder_servehttp", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".servehttp()" + }, + { + "label": "ResponseWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_acme_challenge_go_responsewriter", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "responsewriter" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_acme_challenge_go_request", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "request" + }, + { + "label": "acme/client.go", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_acme_client", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "acme/client.go" + }, + { + "label": "directory", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L18", + "_origin": "ast", + "id": "acme_directory", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "directory" + }, + { + "label": "Client", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L24", + "_origin": "ast", + "id": "acme_client", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "client" + }, + { + "label": "NewClient()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L33", + "_origin": "ast", + "id": "internal_acme_client_newclient", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "newclient()" + }, + { + "label": ".Bootstrap()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L43", + "_origin": "ast", + "id": "acme_client_bootstrap", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".bootstrap()" + }, + { + "label": ".post()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L67", + "_origin": "ast", + "id": "acme_client_post", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".post()" + }, + { + "label": "Response", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "response", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "response" + }, + { + "label": ".NewAccount()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L99", + "_origin": "ast", + "id": "acme_client_newaccount", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".newaccount()" + }, + { + "label": "Order", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L123", + "_origin": "ast", + "id": "acme_order", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "order" + }, + { + "label": ".NewOrder()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L131", + "_origin": "ast", + "id": "acme_client_neworder", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".neworder()" + }, + { + "label": "Authorization", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L157", + "_origin": "ast", + "id": "acme_authorization", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "authorization" + }, + { + "label": "Challenge", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L165", + "_origin": "ast", + "id": "acme_challenge", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "challenge" + }, + { + "label": ".GetAuthorization()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L173", + "_origin": "ast", + "id": "acme_client_getauthorization", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".getauthorization()" + }, + { + "label": ".KeyAuthorization()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L191", + "_origin": "ast", + "id": "acme_client_keyauthorization", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".keyauthorization()" + }, + { + "label": ".RespondToChallenge()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L198", + "_origin": "ast", + "id": "acme_client_respondtochallenge", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".respondtochallenge()" + }, + { + "label": ".WaitForAuthorizationValid()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L211", + "_origin": "ast", + "id": "acme_client_waitforauthorizationvalid", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".waitforauthorizationvalid()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_acme_client_go_duration", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "duration" + }, + { + "label": ".FinalizeAndDownload()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L233", + "_origin": "ast", + "id": "acme_client_finalizeanddownload", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".finalizeanddownload()" + }, + { + "label": "buildCSR()", + "file_type": "code", + "source_file": "internal/acme/client.go", + "source_location": "L295", + "_origin": "ast", + "id": "internal_acme_client_buildcsr", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "buildcsr()" + }, + { + "label": "PrivateKey", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_acme_client_go_privatekey", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "privatekey" + }, + { + "label": "jws.go", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_acme_jws", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "jws.go" + }, + { + "label": "AccountKey", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L25", + "_origin": "ast", + "id": "acme_accountkey", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "accountkey" + }, + { + "label": "PrivateKey", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_acme_jws_go_privatekey", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "privatekey" + }, + { + "label": "GenerateAccountKey()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L29", + "_origin": "ast", + "id": "internal_acme_jws_generateaccountkey", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "generateaccountkey()" + }, + { + "label": ".MarshalPEM()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L37", + "_origin": "ast", + "id": "acme_accountkey_marshalpem", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".marshalpem()" + }, + { + "label": "ParseAccountKeyPEM()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L45", + "_origin": "ast", + "id": "internal_acme_jws_parseaccountkeypem", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "parseaccountkeypem()" + }, + { + "label": "jwk", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L60", + "_origin": "ast", + "id": "acme_jwk", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "jwk" + }, + { + "label": ".jwkValue()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L67", + "_origin": "ast", + "id": "acme_accountkey_jwkvalue", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".jwkvalue()" + }, + { + "label": ".thumbprint()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L78", + "_origin": "ast", + "id": "acme_accountkey_thumbprint", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".thumbprint()" + }, + { + "label": ".signJWS()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L90", + "_origin": "ast", + "id": "acme_accountkey_signjws", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".signjws()" + }, + { + "label": "b64()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L132", + "_origin": "ast", + "id": "internal_acme_jws_b64", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "b64()" + }, + { + "label": "leftPad()", + "file_type": "code", + "source_file": "internal/acme/jws.go", + "source_location": "L136", + "_origin": "ast", + "id": "internal_acme_jws_leftpad", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "leftpad()" + }, + { + "label": "obtain.go", + "file_type": "code", + "source_file": "internal/acme/obtain.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_acme_obtain", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "obtain.go" + }, + { + "label": "Obtain()", + "file_type": "code", + "source_file": "internal/acme/obtain.go", + "source_location": "L14", + "_origin": "ast", + "id": "internal_acme_obtain_obtain", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "obtain()" + }, + { + "label": "admin/api.go", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_admin_api", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "admin/api.go" + }, + { + "label": "Handler", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L25", + "_origin": "ast", + "id": "internal_admin_api_go_admin_handler", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "handler" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_api_go_db", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "db" + }, + { + "label": "NewHandler()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L31", + "_origin": "ast", + "id": "internal_admin_api_newhandler", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "newhandler()" + }, + { + "label": ".RegisterRoutes()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L35", + "_origin": "ast", + "id": "admin_handler_registerroutes", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".registerroutes()" + }, + { + "label": "ServeMux", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_api_go_servemux", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "servemux" + }, + { + "label": "writeJSON()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L53", + "_origin": "ast", + "id": "internal_admin_api_writejson", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "writejson()" + }, + { + "label": "ResponseWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_api_go_responsewriter", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "responsewriter" + }, + { + "label": "writeErr()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L59", + "_origin": "ast", + "id": "internal_admin_api_writeerr", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "writeerr()" + }, + { + "label": ".login()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L65", + "_origin": "ast", + "id": "admin_handler_login", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".login()" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_api_go_request", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "request" + }, + { + "label": ".withAdmin()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L99", + "_origin": "ast", + "id": "admin_handler_withadmin", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".withadmin()" + }, + { + "label": "HandlerFunc", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_api_go_handlerfunc", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "handlerfunc" + }, + { + "label": ".encryptDKIMKey()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L136", + "_origin": "ast", + "id": "admin_handler_encryptdkimkey", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".encryptdkimkey()" + }, + { + "label": ".stats()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L142", + "_origin": "ast", + "id": "admin_handler_stats", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".stats()" + }, + { + "label": ".tenants()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L153", + "_origin": "ast", + "id": "admin_handler_tenants", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".tenants()" + }, + { + "label": ".domains()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L185", + "_origin": "ast", + "id": "admin_handler_domains", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".domains()" + }, + { + "label": ".domainByID()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L239", + "_origin": "ast", + "id": "admin_handler_domainbyid", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".domainbyid()" + }, + { + "label": "scopeTenant()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L293", + "_origin": "ast", + "id": "internal_admin_api_scopetenant", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "scopetenant()" + }, + { + "label": "filterDomainsByTenant()", + "file_type": "code", + "source_file": "internal/admin/api.go", + "source_location": "L300", + "_origin": "ast", + "id": "internal_admin_api_filterdomainsbytenant", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "filterdomainsbytenant()" + }, + { + "label": "admin/embed.go", + "file_type": "code", + "source_file": "internal/admin/embed.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_admin_embed", + "community": 55, + "community_name": "Embedded Assets (admin)", + "norm_label": "admin/embed.go" + }, + { + "label": "handlers.go", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_admin_handlers", + "community": 56, + "community_name": "Admin Handlers Entry", + "norm_label": "handlers.go" + }, + { + "label": "Handler", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L15", + "_origin": "ast", + "id": "internal_admin_handlers_go_admin_handler", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "handler" + }, + { + "label": ".users()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L15", + "_origin": "ast", + "id": "admin_handler_users", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".users()" + }, + { + "label": "ResponseWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_handlers_go_responsewriter", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "responsewriter" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_admin_handlers_go_request", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": "request" + }, + { + "label": ".userByID()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L67", + "_origin": "ast", + "id": "admin_handler_userbyid", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".userbyid()" + }, + { + "label": ".listRules()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L135", + "_origin": "ast", + "id": "admin_handler_listrules", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".listrules()" + }, + { + "label": ".listRuleByID()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L189", + "_origin": "ast", + "id": "admin_handler_listrulebyid", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".listrulebyid()" + }, + { + "label": ".queue()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L204", + "_origin": "ast", + "id": "admin_handler_queue", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".queue()" + }, + { + "label": ".queueByID()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L217", + "_origin": "ast", + "id": "admin_handler_queuebyid", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".queuebyid()" + }, + { + "label": ".quarantine()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L247", + "_origin": "ast", + "id": "admin_handler_quarantine", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".quarantine()" + }, + { + "label": ".quarantineByID()", + "file_type": "code", + "source_file": "internal/admin/handlers.go", + "source_location": "L260", + "_origin": "ast", + "id": "admin_handler_quarantinebyid", + "community": 12, + "community_name": "Admin API Handlers", + "norm_label": ".quarantinebyid()" + }, + { + "label": "auth/auth.go", + "file_type": "code", + "source_file": "internal/auth/auth.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_auth_auth", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "auth/auth.go" + }, + { + "label": "Scope", + "file_type": "code", + "source_file": "internal/auth/auth.go", + "source_location": "L20", + "_origin": "ast", + "id": "auth_scope", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "scope" + }, + { + "label": "Authenticate()", + "file_type": "code", + "source_file": "internal/auth/auth.go", + "source_location": "L33", + "_origin": "ast", + "id": "internal_auth_auth_authenticate", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "authenticate()" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_auth_auth_go_db", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "db" + }, + { + "label": "checkAppPassword()", + "file_type": "code", + "source_file": "internal/auth/auth.go", + "source_location": "L53", + "_origin": "ast", + "id": "internal_auth_auth_checkapppassword", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "checkapppassword()" + }, + { + "label": "config.go", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_config_config", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "config.go" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L12", + "_origin": "ast", + "id": "config_config", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "config" + }, + { + "label": "ServerConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L27", + "_origin": "ast", + "id": "config_serverconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "serverconfig" + }, + { + "label": "TLSConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L42", + "_origin": "ast", + "id": "config_tlsconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "tlsconfig" + }, + { + "label": "DatabaseConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L52", + "_origin": "ast", + "id": "config_databaseconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "databaseconfig" + }, + { + "label": "StorageConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L57", + "_origin": "ast", + "id": "config_storageconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "storageconfig" + }, + { + "label": "RateLimitConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L64", + "_origin": "ast", + "id": "config_ratelimitconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "ratelimitconfig" + }, + { + "label": "PipelineConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L71", + "_origin": "ast", + "id": "config_pipelineconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "pipelineconfig" + }, + { + "label": "NotifyConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L82", + "_origin": "ast", + "id": "config_notifyconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "notifyconfig" + }, + { + "label": "POP3Config", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L90", + "_origin": "ast", + "id": "config_pop3config", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "pop3config" + }, + { + "label": "JMAPConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L96", + "_origin": "ast", + "id": "config_jmapconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "jmapconfig" + }, + { + "label": "OAuthConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L101", + "_origin": "ast", + "id": "config_oauthconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "oauthconfig" + }, + { + "label": "OAuthProviderConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L106", + "_origin": "ast", + "id": "config_oauthproviderconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "oauthproviderconfig" + }, + { + "label": "LinkedAccountsConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L114", + "_origin": "ast", + "id": "config_linkedaccountsconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "linkedaccountsconfig" + }, + { + "label": "SecurityConfig", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L123", + "_origin": "ast", + "id": "config_securityconfig", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "securityconfig" + }, + { + "label": "Load()", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L137", + "_origin": "ast", + "id": "internal_config_config_load", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "load()" + }, + { + "label": "applyEnvOverrides()", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L164", + "_origin": "ast", + "id": "internal_config_config_applyenvoverrides", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "applyenvoverrides()" + }, + { + "label": "validate()", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L188", + "_origin": "ast", + "id": "internal_config_config_validate", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "validate()" + }, + { + "label": "Default()", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L206", + "_origin": "ast", + "id": "internal_config_config_default", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "default()" + }, + { + "label": "writeDefault()", + "file_type": "code", + "source_file": "internal/config/config.go", + "source_location": "L277", + "_origin": "ast", + "id": "internal_config_config_writedefault", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "writedefault()" + }, + { + "label": "crypto.go", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_crypto_crypto", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "crypto.go" + }, + { + "label": "MasterKey", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L27", + "_origin": "ast", + "id": "crypto_masterkey", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "masterkey" + }, + { + "label": "LoadMasterKey()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L33", + "_origin": "ast", + "id": "internal_crypto_crypto_loadmasterkey", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "loadmasterkey()" + }, + { + "label": "decodeKey()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L49", + "_origin": "ast", + "id": "internal_crypto_crypto_decodekey", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "decodekey()" + }, + { + "label": "deriveKey()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L64", + "_origin": "ast", + "id": "internal_crypto_crypto_derivekey", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "derivekey()" + }, + { + "label": "Encrypt()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L78", + "_origin": "ast", + "id": "internal_crypto_crypto_encrypt", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "encrypt()" + }, + { + "label": "Decrypt()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L107", + "_origin": "ast", + "id": "internal_crypto_crypto_decrypt", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "decrypt()" + }, + { + "label": "decryptWith()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L127", + "_origin": "ast", + "id": "internal_crypto_crypto_decryptwith", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "decryptwith()" + }, + { + "label": "NeedsReencryption()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L146", + "_origin": "ast", + "id": "internal_crypto_crypto_needsreencryption", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "needsreencryption()" + }, + { + "label": "GenerateMasterKeyHex()", + "file_type": "code", + "source_file": "internal/crypto/crypto.go", + "source_location": "L160", + "_origin": "ast", + "id": "internal_crypto_crypto_generatemasterkeyhex", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "generatemasterkeyhex()" + }, + { + "label": "dav.go", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_dav_dav", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "dav.go" + }, + { + "label": "Handler", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L36", + "_origin": "ast", + "id": "dav_handler", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "handler" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_dav_dav_go_db", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "db" + }, + { + "label": "NewHandler()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L41", + "_origin": "ast", + "id": "internal_dav_dav_newhandler", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "newhandler()" + }, + { + "label": ".ServeHTTP()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L45", + "_origin": "ast", + "id": "dav_handler_servehttp", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".servehttp()" + }, + { + "label": "ResponseWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_dav_dav_go_responsewriter", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "responsewriter" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_dav_dav_go_request", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "request" + }, + { + "label": ".authenticate()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L64", + "_origin": "ast", + "id": "dav_handler_authenticate", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".authenticate()" + }, + { + "label": ".serveCardDAV()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L74", + "_origin": "ast", + "id": "dav_handler_servecarddav", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".servecarddav()" + }, + { + "label": ".propfindContacts()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L168", + "_origin": "ast", + "id": "dav_handler_propfindcontacts", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".propfindcontacts()" + }, + { + "label": ".reportContacts()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L195", + "_origin": "ast", + "id": "dav_handler_reportcontacts", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".reportcontacts()" + }, + { + "label": ".serveCalDAV()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L223", + "_origin": "ast", + "id": "dav_handler_servecaldav", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".servecaldav()" + }, + { + "label": ".propfindCalendar()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L325", + "_origin": "ast", + "id": "dav_handler_propfindcalendar", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".propfindcalendar()" + }, + { + "label": ".reportCalendar()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L352", + "_origin": "ast", + "id": "dav_handler_reportcalendar", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".reportcalendar()" + }, + { + "label": "parseCollectionPath()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L385", + "_origin": "ast", + "id": "internal_dav_dav_parsecollectionpath", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "parsecollectionpath()" + }, + { + "label": "propSet", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L414", + "_origin": "ast", + "id": "dav_propset", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "propset" + }, + { + "label": "multistatusResponse", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L421", + "_origin": "ast", + "id": "dav_multistatusresponse", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "multistatusresponse" + }, + { + "label": "writeMultistatus()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L428", + "_origin": "ast", + "id": "internal_dav_dav_writemultistatus", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "writemultistatus()" + }, + { + "label": "xmlEscape()", + "file_type": "code", + "source_file": "internal/dav/dav.go", + "source_location": "L467", + "_origin": "ast", + "id": "internal_dav_dav_xmlescape", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "xmlescape()" + }, + { + "label": "bootstrap.go", + "file_type": "code", + "source_file": "internal/db/bootstrap.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_db_bootstrap", + "community": 59, + "community_name": "Webmail Bootstrap", + "norm_label": "bootstrap.go" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "internal/db/bootstrap.go", + "source_location": "L16", + "_origin": "ast", + "id": "internal_db_bootstrap_go_db_db", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "db" + }, + { + "label": ".Bootstrap()", + "file_type": "code", + "source_file": "internal/db/bootstrap.go", + "source_location": "L16", + "_origin": "ast", + "id": "db_db_bootstrap", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".bootstrap()" + }, + { + "label": "db.go", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_db_db", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "db.go" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L16", + "_origin": "ast", + "id": "internal_db_db_go_db_db", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "db" + }, + { + "label": "Open()", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L26", + "_origin": "ast", + "id": "internal_db_db_open", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "open()" + }, + { + "label": "registerDriver()", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L76", + "_origin": "ast", + "id": "internal_db_db_registerdriver", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "registerdriver()" + }, + { + "label": ".Migrate()", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L83", + "_origin": "ast", + "id": "db_db_migrate", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": ".migrate()" + }, + { + "label": ".migrationApplied()", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L126", + "_origin": "ast", + "id": "db_db_migrationapplied", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": ".migrationapplied()" + }, + { + "label": ".insertMigrationSQL()", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L132", + "_origin": "ast", + "id": "db_db_insertmigrationsql", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": ".insertmigrationsql()" + }, + { + "label": ".placeholder()", + "file_type": "code", + "source_file": "internal/db/db.go", + "source_location": "L138", + "_origin": "ast", + "id": "db_db_placeholder", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": ".placeholder()" + }, + { + "label": "migrations.go", + "file_type": "code", + "source_file": "internal/db/migrations.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_db_migrations", + "community": 50, + "community_name": "DB Migrations", + "norm_label": "migrations.go" + }, + { + "label": "migration", + "file_type": "code", + "source_file": "internal/db/migrations.go", + "source_location": "L7", + "_origin": "ast", + "id": "db_migration", + "community": 50, + "community_name": "DB Migrations", + "norm_label": "migration" + }, + { + "label": "models.go", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_db_models", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "models.go" + }, + { + "label": "Tenant", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L7", + "_origin": "ast", + "id": "db_tenant", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": "tenant" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_db_models_go_time", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "time" + }, + { + "label": "Domain", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L18", + "_origin": "ast", + "id": "db_domain", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": "domain" + }, + { + "label": "UserRole", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L31", + "_origin": "ast", + "id": "db_userrole", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "userrole" + }, + { + "label": "User", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L39", + "_origin": "ast", + "id": "db_user", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "user" + }, + { + "label": "AppPassword", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L61", + "_origin": "ast", + "id": "db_apppassword", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "apppassword" + }, + { + "label": "Session", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L72", + "_origin": "ast", + "id": "db_session", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "session" + }, + { + "label": "Alias", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L82", + "_origin": "ast", + "id": "db_alias", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "alias" + }, + { + "label": "ListRuleAction", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L93", + "_origin": "ast", + "id": "db_listruleaction", + "community": 42, + "community_name": "List Rule Queries", + "norm_label": "listruleaction" + }, + { + "label": "ListRule", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L100", + "_origin": "ast", + "id": "db_listrule", + "community": 42, + "community_name": "List Rule Queries", + "norm_label": "listrule" + }, + { + "label": "MessageVerdict", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L113", + "_origin": "ast", + "id": "db_messageverdict", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": "messageverdict" + }, + { + "label": "Message", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L122", + "_origin": "ast", + "id": "db_message", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": "message" + }, + { + "label": "MailboxEntry", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L137", + "_origin": "ast", + "id": "db_mailboxentry", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "mailboxentry" + }, + { + "label": "OutboundQueueEntry", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L151", + "_origin": "ast", + "id": "db_outboundqueueentry", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": "outboundqueueentry" + }, + { + "label": "CheckResult", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L167", + "_origin": "ast", + "id": "db_checkresult", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "checkresult" + }, + { + "label": "MessageCheck", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L177", + "_origin": "ast", + "id": "db_messagecheck", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "messagecheck" + }, + { + "label": "QuarantineStatus", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L189", + "_origin": "ast", + "id": "db_quarantinestatus", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": "quarantinestatus" + }, + { + "label": "QuarantineEntry", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L197", + "_origin": "ast", + "id": "db_quarantineentry", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": "quarantineentry" + }, + { + "label": "ReleaseToken", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L210", + "_origin": "ast", + "id": "db_releasetoken", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "releasetoken" + }, + { + "label": "LinkedAccountProvider", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L222", + "_origin": "ast", + "id": "db_linkedaccountprovider", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "linkedaccountprovider" + }, + { + "label": "LinkedAccountAuthType", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L231", + "_origin": "ast", + "id": "db_linkedaccountauthtype", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "linkedaccountauthtype" + }, + { + "label": "LinkedAccount", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L239", + "_origin": "ast", + "id": "db_linkedaccount", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "linkedaccount" + }, + { + "label": "OwnerType", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L267", + "_origin": "ast", + "id": "db_ownertype", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "ownertype" + }, + { + "label": "Addressbook", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L274", + "_origin": "ast", + "id": "db_addressbook", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "addressbook" + }, + { + "label": "Contact", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L284", + "_origin": "ast", + "id": "db_contact", + "community": 46, + "community_name": "Contact Queries", + "norm_label": "contact" + }, + { + "label": "Calendar", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L294", + "_origin": "ast", + "id": "db_calendar", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "calendar" + }, + { + "label": "CalendarObject", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L306", + "_origin": "ast", + "id": "db_calendarobject", + "community": 45, + "community_name": "Calendar Object Queries", + "norm_label": "calendarobject" + }, + { + "label": "SieveScript", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L322", + "_origin": "ast", + "id": "db_sievescript", + "community": 43, + "community_name": "Sieve Script Queries", + "norm_label": "sievescript" + }, + { + "label": "TLSCert", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L334", + "_origin": "ast", + "id": "db_tlscert", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "tlscert" + }, + { + "label": "MFABackupCode", + "file_type": "code", + "source_file": "internal/db/models.go", + "source_location": "L347", + "_origin": "ast", + "id": "db_mfabackupcode", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": "mfabackupcode" + }, + { + "label": "queries.go", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_db_queries", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "queries.go" + }, + { + "label": "uuidNew()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L11", + "_origin": "ast", + "id": "internal_db_queries_uuidnew", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "uuidnew()" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L15", + "_origin": "ast", + "id": "internal_db_queries_go_db_db", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": "db" + }, + { + "label": ".LookupDomain()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L15", + "_origin": "ast", + "id": "db_db_lookupdomain", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".lookupdomain()" + }, + { + "label": ".LookupUserByEmail()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L43", + "_origin": "ast", + "id": "db_db_lookupuserbyemail", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".lookupuserbyemail()" + }, + { + "label": ".LookupAlias()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L68", + "_origin": "ast", + "id": "db_db_lookupalias", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".lookupalias()" + }, + { + "label": ".MatchListRule()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L88", + "_origin": "ast", + "id": "db_db_matchlistrule", + "community": 42, + "community_name": "List Rule Queries", + "norm_label": ".matchlistrule()" + }, + { + "label": ".InsertMessage()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L122", + "_origin": "ast", + "id": "db_db_insertmessage", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".insertmessage()" + }, + { + "label": ".UpdateMessageVerdict()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L139", + "_origin": "ast", + "id": "db_db_updatemessageverdict", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".updatemessageverdict()" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_db_queries_go_time", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": "time" + }, + { + "label": ".InsertMessageCheck()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L146", + "_origin": "ast", + "id": "db_db_insertmessagecheck", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".insertmessagecheck()" + }, + { + "label": ".InsertQuarantineEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L160", + "_origin": "ast", + "id": "db_db_insertquarantineentry", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".insertquarantineentry()" + }, + { + "label": ".QuarantineEntriesForUser()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L174", + "_origin": "ast", + "id": "db_db_quarantineentriesforuser", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".quarantineentriesforuser()" + }, + { + "label": ".ReleaseQuarantineEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L203", + "_origin": "ast", + "id": "db_db_releasequarantineentry", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".releasequarantineentry()" + }, + { + "label": ".GetQuarantineEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L210", + "_origin": "ast", + "id": "db_db_getquarantineentry", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".getquarantineentry()" + }, + { + "label": ".InsertReleaseToken()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L230", + "_origin": "ast", + "id": "db_db_insertreleasetoken", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".insertreleasetoken()" + }, + { + "label": ".LookupReleaseToken()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L238", + "_origin": "ast", + "id": "db_db_lookupreleasetoken", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".lookupreleasetoken()" + }, + { + "label": ".MarkReleaseTokenUsed()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L254", + "_origin": "ast", + "id": "db_db_markreleasetokenused", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".markreleasetokenused()" + }, + { + "label": ".NextMailboxUID()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L260", + "_origin": "ast", + "id": "db_db_nextmailboxuid", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".nextmailboxuid()" + }, + { + "label": ".InsertMailboxEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L288", + "_origin": "ast", + "id": "db_db_insertmailboxentry", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".insertmailboxentry()" + }, + { + "label": ".ListMailboxEntries()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L301", + "_origin": "ast", + "id": "db_db_listmailboxentries", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".listmailboxentries()" + }, + { + "label": ".ListMailboxNames()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L324", + "_origin": "ast", + "id": "db_db_listmailboxnames", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".listmailboxnames()" + }, + { + "label": ".UpdateMailboxFlags()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L347", + "_origin": "ast", + "id": "db_db_updatemailboxflags", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".updatemailboxflags()" + }, + { + "label": ".DeleteMailboxEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L354", + "_origin": "ast", + "id": "db_db_deletemailboxentry", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletemailboxentry()" + }, + { + "label": ".InsertLinkedAccount()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L361", + "_origin": "ast", + "id": "db_db_insertlinkedaccount", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".insertlinkedaccount()" + }, + { + "label": ".ListLinkedAccounts()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L374", + "_origin": "ast", + "id": "db_db_listlinkedaccounts", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".listlinkedaccounts()" + }, + { + "label": ".GetLinkedAccount()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L410", + "_origin": "ast", + "id": "db_db_getlinkedaccount", + "community": 17, + "community_name": "Linked Account & Alias Queries", + "norm_label": ".getlinkedaccount()" + }, + { + "label": ".UpdateLinkedAccountSync()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L441", + "_origin": "ast", + "id": "db_db_updatelinkedaccountsync", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".updatelinkedaccountsync()" + }, + { + "label": ".DeactivateLinkedAccount()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L447", + "_origin": "ast", + "id": "db_db_deactivatelinkedaccount", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deactivatelinkedaccount()" + }, + { + "label": ".ListTenants()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L456", + "_origin": "ast", + "id": "db_db_listtenants", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".listtenants()" + }, + { + "label": ".CreateTenant()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L475", + "_origin": "ast", + "id": "db_db_createtenant", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".createtenant()" + }, + { + "label": ".ListDomains()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L482", + "_origin": "ast", + "id": "db_db_listdomains", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".listdomains()" + }, + { + "label": ".CreateDomain()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L501", + "_origin": "ast", + "id": "db_db_createdomain", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".createdomain()" + }, + { + "label": ".UpdateDomainDKIMKey()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L507", + "_origin": "ast", + "id": "db_db_updatedomaindkimkey", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".updatedomaindkimkey()" + }, + { + "label": ".DeleteDomain()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L512", + "_origin": "ast", + "id": "db_db_deletedomain", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletedomain()" + }, + { + "label": ".GetDomain()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L517", + "_origin": "ast", + "id": "db_db_getdomain", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".getdomain()" + }, + { + "label": ".ListUsers()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L536", + "_origin": "ast", + "id": "db_db_listusers", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".listusers()" + }, + { + "label": ".CreateUser()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L563", + "_origin": "ast", + "id": "db_db_createuser", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".createuser()" + }, + { + "label": ".SetUserActive()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L569", + "_origin": "ast", + "id": "db_db_setuseractive", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".setuseractive()" + }, + { + "label": ".SetUserPassword()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L574", + "_origin": "ast", + "id": "db_db_setuserpassword", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".setuserpassword()" + }, + { + "label": ".DeleteUser()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L579", + "_origin": "ast", + "id": "db_db_deleteuser", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deleteuser()" + }, + { + "label": ".GetUser()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L584", + "_origin": "ast", + "id": "db_db_getuser", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".getuser()" + }, + { + "label": ".ListListRules()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L604", + "_origin": "ast", + "id": "db_db_listlistrules", + "community": 42, + "community_name": "List Rule Queries", + "norm_label": ".listlistrules()" + }, + { + "label": ".CreateListRule()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L623", + "_origin": "ast", + "id": "db_db_createlistrule", + "community": 42, + "community_name": "List Rule Queries", + "norm_label": ".createlistrule()" + }, + { + "label": ".DeleteListRule()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L629", + "_origin": "ast", + "id": "db_db_deletelistrule", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletelistrule()" + }, + { + "label": ".ListAllOutboundQueue()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L636", + "_origin": "ast", + "id": "db_db_listalloutboundqueue", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".listalloutboundqueue()" + }, + { + "label": ".RetryQueueEntryNow()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L657", + "_origin": "ast", + "id": "db_db_retryqueueentrynow", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".retryqueueentrynow()" + }, + { + "label": ".DeleteQuarantineEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L667", + "_origin": "ast", + "id": "db_db_deletequarantineentry", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletequarantineentry()" + }, + { + "label": ".ListAllQuarantine()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L672", + "_origin": "ast", + "id": "db_db_listallquarantine", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".listallquarantine()" + }, + { + "label": "Stats", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L693", + "_origin": "ast", + "id": "db_stats", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": "stats" + }, + { + "label": ".GetStats()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L701", + "_origin": "ast", + "id": "db_db_getstats", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".getstats()" + }, + { + "label": ".GetOrCreateAddressbook()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L713", + "_origin": "ast", + "id": "db_db_getorcreateaddressbook", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".getorcreateaddressbook()" + }, + { + "label": ".GetOrCreateCalendar()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L735", + "_origin": "ast", + "id": "db_db_getorcreatecalendar", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".getorcreatecalendar()" + }, + { + "label": ".ListContacts()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L758", + "_origin": "ast", + "id": "db_db_listcontacts", + "community": 46, + "community_name": "Contact Queries", + "norm_label": ".listcontacts()" + }, + { + "label": ".GetContact()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L776", + "_origin": "ast", + "id": "db_db_getcontact", + "community": 46, + "community_name": "Contact Queries", + "norm_label": ".getcontact()" + }, + { + "label": ".UpsertContact()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L793", + "_origin": "ast", + "id": "db_db_upsertcontact", + "community": 46, + "community_name": "Contact Queries", + "norm_label": ".upsertcontact()" + }, + { + "label": ".DeleteContact()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L805", + "_origin": "ast", + "id": "db_db_deletecontact", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletecontact()" + }, + { + "label": ".ListCalendarObjects()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L810", + "_origin": "ast", + "id": "db_db_listcalendarobjects", + "community": 45, + "community_name": "Calendar Object Queries", + "norm_label": ".listcalendarobjects()" + }, + { + "label": ".GetCalendarObject()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L838", + "_origin": "ast", + "id": "db_db_getcalendarobject", + "community": 45, + "community_name": "Calendar Object Queries", + "norm_label": ".getcalendarobject()" + }, + { + "label": ".UpsertCalendarObject()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L862", + "_origin": "ast", + "id": "db_db_upsertcalendarobject", + "community": 45, + "community_name": "Calendar Object Queries", + "norm_label": ".upsertcalendarobject()" + }, + { + "label": ".DeleteCalendarObject()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L875", + "_origin": "ast", + "id": "db_db_deletecalendarobject", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletecalendarobject()" + }, + { + "label": ".ListSieveScripts()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L882", + "_origin": "ast", + "id": "db_db_listsievescripts", + "community": 43, + "community_name": "Sieve Script Queries", + "norm_label": ".listsievescripts()" + }, + { + "label": ".GetSieveScript()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L900", + "_origin": "ast", + "id": "db_db_getsievescript", + "community": 43, + "community_name": "Sieve Script Queries", + "norm_label": ".getsievescript()" + }, + { + "label": ".GetActiveSieveScript()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L916", + "_origin": "ast", + "id": "db_db_getactivesievescript", + "community": 43, + "community_name": "Sieve Script Queries", + "norm_label": ".getactivesievescript()" + }, + { + "label": ".UpsertSieveScript()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L930", + "_origin": "ast", + "id": "db_db_upsertsievescript", + "community": 43, + "community_name": "Sieve Script Queries", + "norm_label": ".upsertsievescript()" + }, + { + "label": ".SetActiveSieveScript()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L945", + "_origin": "ast", + "id": "db_db_setactivesievescript", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".setactivesievescript()" + }, + { + "label": ".DeleteSieveScript()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L964", + "_origin": "ast", + "id": "db_db_deletesievescript", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deletesievescript()" + }, + { + "label": ".GetTLSCert()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L971", + "_origin": "ast", + "id": "db_db_gettlscert", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".gettlscert()" + }, + { + "label": ".UpsertTLSCert()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L986", + "_origin": "ast", + "id": "db_db_upserttlscert", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".upserttlscert()" + }, + { + "label": ".SetACMEAccountKey()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1001", + "_origin": "ast", + "id": "db_db_setacmeaccountkey", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".setacmeaccountkey()" + }, + { + "label": ".SetPendingTOTPSecret()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1016", + "_origin": "ast", + "id": "db_db_setpendingtotpsecret", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".setpendingtotpsecret()" + }, + { + "label": ".SetMFAEnabled()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1021", + "_origin": "ast", + "id": "db_db_setmfaenabled", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".setmfaenabled()" + }, + { + "label": ".ClearTOTPSecret()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1026", + "_origin": "ast", + "id": "db_db_cleartotpsecret", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".cleartotpsecret()" + }, + { + "label": ".ReplaceBackupCodes()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1034", + "_origin": "ast", + "id": "db_db_replacebackupcodes", + "community": 19, + "community_name": "Calendar/Contact/TLS Queries", + "norm_label": ".replacebackupcodes()" + }, + { + "label": ".ConsumeBackupCode()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1054", + "_origin": "ast", + "id": "db_db_consumebackupcode", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".consumebackupcode()" + }, + { + "label": ".SetRecoveryEmail()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1086", + "_origin": "ast", + "id": "db_db_setrecoveryemail", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".setrecoveryemail()" + }, + { + "label": ".InsertOutboundQueueEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1094", + "_origin": "ast", + "id": "db_db_insertoutboundqueueentry", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".insertoutboundqueueentry()" + }, + { + "label": ".DueOutboundEntries()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1107", + "_origin": "ast", + "id": "db_db_dueoutboundentries", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".dueoutboundentries()" + }, + { + "label": ".DeleteOutboundEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1136", + "_origin": "ast", + "id": "db_db_deleteoutboundentry", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".deleteoutboundentry()" + }, + { + "label": ".RetryOutboundEntry()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1142", + "_origin": "ast", + "id": "db_db_retryoutboundentry", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".retryoutboundentry()" + }, + { + "label": ".PermanentlyFailedEntries()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1152", + "_origin": "ast", + "id": "db_db_permanentlyfailedentries", + "community": 7, + "community_name": "DB Mutation Queries", + "norm_label": ".permanentlyfailedentries()" + }, + { + "label": ".LookupDomainByName()", + "file_type": "code", + "source_file": "internal/db/queries.go", + "source_location": "L1178", + "_origin": "ast", + "id": "db_db_lookupdomainbyname", + "community": 31, + "community_name": "Domain/Tenant Queries", + "norm_label": ".lookupdomainbyname()" + }, + { + "label": "keys.go", + "file_type": "code", + "source_file": "internal/dkim/keys.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_dkim_keys", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "keys.go" + }, + { + "label": "KeyPair", + "file_type": "code", + "source_file": "internal/dkim/keys.go", + "source_location": "L17", + "_origin": "ast", + "id": "dkim_keypair", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "keypair" + }, + { + "label": "GenerateKeyPair()", + "file_type": "code", + "source_file": "internal/dkim/keys.go", + "source_location": "L25", + "_origin": "ast", + "id": "internal_dkim_keys_generatekeypair", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "generatekeypair()" + }, + { + "label": "ParsePrivateKey()", + "file_type": "code", + "source_file": "internal/dkim/keys.go", + "source_location": "L53", + "_origin": "ast", + "id": "internal_dkim_keys_parseprivatekey", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "parseprivatekey()" + }, + { + "label": "PrivateKey", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_dkim_keys_go_privatekey", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "privatekey" + }, + { + "label": "ExtractSignatureInfo()", + "file_type": "code", + "source_file": "internal/dkim/keys.go", + "source_location": "L69", + "_origin": "ast", + "id": "internal_dkim_keys_extractsignatureinfo", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "extractsignatureinfo()" + }, + { + "label": "ParseDNSPublicKey()", + "file_type": "code", + "source_file": "internal/dkim/keys.go", + "source_location": "L85", + "_origin": "ast", + "id": "internal_dkim_keys_parsednspublickey", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "parsednspublickey()" + }, + { + "label": "sign.go", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_dkim_sign", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "sign.go" + }, + { + "label": "Sign()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L24", + "_origin": "ast", + "id": "internal_dkim_sign_sign", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "sign()" + }, + { + "label": "buildDKIMHeader()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L77", + "_origin": "ast", + "id": "internal_dkim_sign_builddkimheader", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "builddkimheader()" + }, + { + "label": "splitMessage()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L86", + "_origin": "ast", + "id": "internal_dkim_sign_splitmessage", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "splitmessage()" + }, + { + "label": "parseHeaders()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L103", + "_origin": "ast", + "id": "internal_dkim_sign_parseheaders", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "parseheaders()" + }, + { + "label": "canonicalizeHeadersRelaxed()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L139", + "_origin": "ast", + "id": "internal_dkim_sign_canonicalizeheadersrelaxed", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "canonicalizeheadersrelaxed()" + }, + { + "label": "canonicalizeHeaderRelaxed()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L151", + "_origin": "ast", + "id": "internal_dkim_sign_canonicalizeheaderrelaxed", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "canonicalizeheaderrelaxed()" + }, + { + "label": "collapseWSP()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L159", + "_origin": "ast", + "id": "internal_dkim_sign_collapsewsp", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "collapsewsp()" + }, + { + "label": "canonicalizeBodyRelaxed()", + "file_type": "code", + "source_file": "internal/dkim/sign.go", + "source_location": "L167", + "_origin": "ast", + "id": "internal_dkim_sign_canonicalizebodyrelaxed", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "canonicalizebodyrelaxed()" + }, + { + "label": "verify.go", + "file_type": "code", + "source_file": "internal/dkim/verify.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_dkim_verify", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "verify.go" + }, + { + "label": "Verify()", + "file_type": "code", + "source_file": "internal/dkim/verify.go", + "source_location": "L19", + "_origin": "ast", + "id": "internal_dkim_verify_verify", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "verify()" + }, + { + "label": "parseDKIMTags()", + "file_type": "code", + "source_file": "internal/dkim/verify.go", + "source_location": "L76", + "_origin": "ast", + "id": "internal_dkim_verify_parsedkimtags", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "parsedkimtags()" + }, + { + "label": "replaceDKIMTag()", + "file_type": "code", + "source_file": "internal/dkim/verify.go", + "source_location": "L92", + "_origin": "ast", + "id": "internal_dkim_verify_replacedkimtag", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "replacedkimtag()" + }, + { + "label": "trimTrailingCRLF()", + "file_type": "code", + "source_file": "internal/dkim/verify.go", + "source_location": "L103", + "_origin": "ast", + "id": "internal_dkim_verify_trimtrailingcrlf", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "trimtrailingcrlf()" + }, + { + "label": "ical.go", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_ical_ical", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "ical.go" + }, + { + "label": "Event", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L17", + "_origin": "ast", + "id": "ical_event", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "event" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_ical_ical_go_time", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "time" + }, + { + "label": "Parse()", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L27", + "_origin": "ast", + "id": "internal_ical_ical_parse", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "parse()" + }, + { + "label": ".Build()", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L82", + "_origin": "ast", + "id": "ical_event_build", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": ".build()" + }, + { + "label": "splitProperty()", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L109", + "_origin": "ast", + "id": "internal_ical_ical_splitproperty", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "splitproperty()" + }, + { + "label": "unfold()", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L123", + "_origin": "ast", + "id": "internal_ical_ical_unfold", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "unfold()" + }, + { + "label": "escape()", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L136", + "_origin": "ast", + "id": "internal_ical_ical_escape", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "escape()" + }, + { + "label": "unescape()", + "file_type": "code", + "source_file": "internal/ical/ical.go", + "source_location": "L144", + "_origin": "ast", + "id": "internal_ical_ical_unescape", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "unescape()" + }, + { + "label": "ical_fuzz_test.go", + "file_type": "code", + "source_file": "internal/ical/ical_fuzz_test.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_ical_ical_fuzz_test", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "ical_fuzz_test.go" + }, + { + "label": "FuzzParse()", + "file_type": "code", + "source_file": "internal/ical/ical_fuzz_test.go", + "source_location": "L5", + "_origin": "ast", + "id": "internal_ical_ical_fuzz_test_fuzzparse", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "fuzzparse()" + }, + { + "label": "F", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_ical_ical_fuzz_test_go_f", + "community": 24, + "community_name": "iCal Parsing & Fuzzing", + "norm_label": "f" + }, + { + "label": "commands.go", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_imap_commands", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "commands.go" + }, + { + "label": "session", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L11", + "_origin": "ast", + "id": "internal_imap_commands_go_imap_session", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "session" + }, + { + "label": ".cmdCapability()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L11", + "_origin": "ast", + "id": "imap_session_cmdcapability", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdcapability()" + }, + { + "label": ".cmdStartTLS()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L22", + "_origin": "ast", + "id": "imap_session_cmdstarttls", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdstarttls()" + }, + { + "label": ".cmdLogin()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L34", + "_origin": "ast", + "id": "imap_session_cmdlogin", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdlogin()" + }, + { + "label": ".cmdSelectExamine()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L52", + "_origin": "ast", + "id": "imap_session_cmdselectexamine", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdselectexamine()" + }, + { + "label": ".cmdList()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L99", + "_origin": "ast", + "id": "imap_session_cmdlist", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdlist()" + }, + { + "label": ".cmdClose()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L118", + "_origin": "ast", + "id": "imap_session_cmdclose", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdclose()" + }, + { + "label": ".cmdExpunge()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L129", + "_origin": "ast", + "id": "imap_session_cmdexpunge", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdexpunge()" + }, + { + "label": ".expungeDeleted()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L146", + "_origin": "ast", + "id": "imap_session_expungedeleted", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".expungedeleted()" + }, + { + "label": ".cmdUID()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L170", + "_origin": "ast", + "id": "imap_session_cmduid", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmduid()" + }, + { + "label": ".cmdFetch()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L192", + "_origin": "ast", + "id": "imap_session_cmdfetch", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdfetch()" + }, + { + "label": "expandFetchItems()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L211", + "_origin": "ast", + "id": "internal_imap_commands_expandfetchitems", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "expandfetchitems()" + }, + { + "label": ".sendFetchResponse()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L234", + "_origin": "ast", + "id": "imap_session_sendfetchresponse", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".sendfetchresponse()" + }, + { + "label": "extractHeaders()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L285", + "_origin": "ast", + "id": "internal_imap_commands_extractheaders", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "extractheaders()" + }, + { + "label": "indexOf()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L293", + "_origin": "ast", + "id": "internal_imap_commands_indexof", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "indexof()" + }, + { + "label": ".cmdStore()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L311", + "_origin": "ast", + "id": "imap_session_cmdstore", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdstore()" + }, + { + "label": "addFlag()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L359", + "_origin": "ast", + "id": "internal_imap_commands_addflag", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "addflag()" + }, + { + "label": "removeFlag()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L369", + "_origin": "ast", + "id": "internal_imap_commands_removeflag", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "removeflag()" + }, + { + "label": "flagsToIMAP()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L380", + "_origin": "ast", + "id": "internal_imap_commands_flagstoimap", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "flagstoimap()" + }, + { + "label": ".cmdSearch()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L386", + "_origin": "ast", + "id": "imap_session_cmdsearch", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".cmdsearch()" + }, + { + "label": "matchesSearch()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L418", + "_origin": "ast", + "id": "internal_imap_commands_matchessearch", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "matchessearch()" + }, + { + "label": ".resolveSequenceSet()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L453", + "_origin": "ast", + "id": "imap_session_resolvesequenceset", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".resolvesequenceset()" + }, + { + "label": "parseSeqNum()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L487", + "_origin": "ast", + "id": "internal_imap_commands_parseseqnum", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "parseseqnum()" + }, + { + "label": "quoteIfNeeded()", + "file_type": "code", + "source_file": "internal/imap/commands.go", + "source_location": "L504", + "_origin": "ast", + "id": "internal_imap_commands_quoteifneeded", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "quoteifneeded()" + }, + { + "label": "imap/parser.go", + "file_type": "code", + "source_file": "internal/imap/parser.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_imap_parser", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "imap/parser.go" + }, + { + "label": "tokenize()", + "file_type": "code", + "source_file": "internal/imap/parser.go", + "source_location": "L11", + "_origin": "ast", + "id": "internal_imap_parser_tokenize", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "tokenize()" + }, + { + "label": "splitList()", + "file_type": "code", + "source_file": "internal/imap/parser.go", + "source_location": "L66", + "_origin": "ast", + "id": "internal_imap_parser_splitlist", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "splitlist()" + }, + { + "label": "isList()", + "file_type": "code", + "source_file": "internal/imap/parser.go", + "source_location": "L75", + "_origin": "ast", + "id": "internal_imap_parser_islist", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "islist()" + }, + { + "label": "imap/server.go", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_imap_server", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "imap/server.go" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L33", + "_origin": "ast", + "id": "imap_server", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "server" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_db", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "db" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_config", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "config" + }, + { + "label": "Listener", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_listener", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "listener" + }, + { + "label": "WaitGroup", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_waitgroup", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "waitgroup" + }, + { + "label": "Limiter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_limiter", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "limiter" + }, + { + "label": "NewServer()", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L46", + "_origin": "ast", + "id": "internal_imap_server_newserver", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "newserver()" + }, + { + "label": ".ListenAndServe()", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L52", + "_origin": "ast", + "id": "imap_server_listenandserve", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": ".listenandserve()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_context", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "context" + }, + { + "label": ".acceptLoop()", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L84", + "_origin": "ast", + "id": "imap_server_acceptloop", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": ".acceptloop()" + }, + { + "label": ".Shutdown()", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L112", + "_origin": "ast", + "id": "imap_server_shutdown", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": ".shutdown()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_duration", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "duration" + }, + { + "label": ".closeAll()", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L127", + "_origin": "ast", + "id": "imap_server_closeall", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": ".closeall()" + }, + { + "label": "connHost()", + "file_type": "code", + "source_file": "internal/imap/server.go", + "source_location": "L136", + "_origin": "ast", + "id": "internal_imap_server_connhost", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "connhost()" + }, + { + "label": "Addr", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_server_go_addr", + "community": 18, + "community_name": "TCP Server Lifecycle", + "norm_label": "addr" + }, + { + "label": "imap/session.go", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_imap_session", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "imap/session.go" + }, + { + "label": "state", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L18", + "_origin": "ast", + "id": "imap_state", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "state" + }, + { + "label": "session", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L26", + "_origin": "ast", + "id": "internal_imap_session_go_imap_session", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "session" + }, + { + "label": "Conn", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_session_go_conn", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "conn" + }, + { + "label": "ReadWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_session_go_readwriter", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "readwriter" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_session_go_server", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "server" + }, + { + "label": "newSession()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L46", + "_origin": "ast", + "id": "internal_imap_session_newsession", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "newsession()" + }, + { + "label": ".run()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L57", + "_origin": "ast", + "id": "imap_session_run", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_session_go_context", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "context" + }, + { + "label": ".readCommand()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L88", + "_origin": "ast", + "id": "imap_session_readcommand", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".readcommand()" + }, + { + "label": ".dispatch()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L106", + "_origin": "ast", + "id": "imap_session_dispatch", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".dispatch()" + }, + { + "label": ".requireAuthenticated()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L153", + "_origin": "ast", + "id": "imap_session_requireauthenticated", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".requireauthenticated()" + }, + { + "label": ".requireSelected()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L161", + "_origin": "ast", + "id": "imap_session_requireselected", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".requireselected()" + }, + { + "label": ".tagged()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L171", + "_origin": "ast", + "id": "imap_session_tagged", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".tagged()" + }, + { + "label": ".untagged()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L176", + "_origin": "ast", + "id": "imap_session_untagged", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".untagged()" + }, + { + "label": ".continuation()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L181", + "_origin": "ast", + "id": "imap_session_continuation", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".continuation()" + }, + { + "label": ".upgradeTLS()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L186", + "_origin": "ast", + "id": "imap_session_upgradetls", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": ".upgradetls()" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_session_go_config", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "config" + }, + { + "label": ".authenticateUser()", + "file_type": "code", + "source_file": "internal/imap/session.go", + "source_location": "L197", + "_origin": "ast", + "id": "imap_session_authenticateuser", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".authenticateuser()" + }, + { + "label": "tokenize_fuzz_test.go", + "file_type": "code", + "source_file": "internal/imap/tokenize_fuzz_test.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_imap_tokenize_fuzz_test", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "tokenize_fuzz_test.go" + }, + { + "label": "FuzzTokenize()", + "file_type": "code", + "source_file": "internal/imap/tokenize_fuzz_test.go", + "source_location": "L5", + "_origin": "ast", + "id": "internal_imap_tokenize_fuzz_test_fuzztokenize", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "fuzztokenize()" + }, + { + "label": "F", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imap_tokenize_fuzz_test_go_f", + "community": 3, + "community_name": "IMAP Command Parser", + "norm_label": "f" + }, + { + "label": "imapclient/client.go", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_imapclient_client", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "imapclient/client.go" + }, + { + "label": "Client", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L21", + "_origin": "ast", + "id": "imapclient_client", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "client" + }, + { + "label": "Conn", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imapclient_client_go_conn", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "conn" + }, + { + "label": "Reader", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "reader", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "reader" + }, + { + "label": "Writer", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imapclient_client_go_writer", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "writer" + }, + { + "label": "Dial()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L31", + "_origin": "ast", + "id": "internal_imapclient_client_dial", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "dial()" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imapclient_client_go_config", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "config" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_imapclient_client_go_duration", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "duration" + }, + { + "label": ".StartTLS()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L49", + "_origin": "ast", + "id": "imapclient_client_starttls", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".starttls()" + }, + { + "label": ".Login()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L60", + "_origin": "ast", + "id": "imapclient_client_login", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".login()" + }, + { + "label": ".LoginXOAUTH2()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L69", + "_origin": "ast", + "id": "imapclient_client_loginxoauth2", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".loginxoauth2()" + }, + { + "label": ".Logout()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L81", + "_origin": "ast", + "id": "imapclient_client_logout", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".logout()" + }, + { + "label": "FolderInfo", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L87", + "_origin": "ast", + "id": "imapclient_folderinfo", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "folderinfo" + }, + { + "label": ".List()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L91", + "_origin": "ast", + "id": "imapclient_client_list", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".list()" + }, + { + "label": "SelectedInfo", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L118", + "_origin": "ast", + "id": "imapclient_selectedinfo", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "selectedinfo" + }, + { + "label": ".Select()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L122", + "_origin": "ast", + "id": "imapclient_client_select", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".select()" + }, + { + "label": "FetchedMessage", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L142", + "_origin": "ast", + "id": "imapclient_fetchedmessage", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "fetchedmessage" + }, + { + "label": ".Fetch()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L151", + "_origin": "ast", + "id": "imapclient_client_fetch", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".fetch()" + }, + { + "label": ".UIDFetch()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L165", + "_origin": "ast", + "id": "imapclient_client_uidfetch", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".uidfetch()" + }, + { + "label": ".Store()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L176", + "_origin": "ast", + "id": "imapclient_client_store", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".store()" + }, + { + "label": ".UIDStore()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L181", + "_origin": "ast", + "id": "imapclient_client_uidstore", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".uidstore()" + }, + { + "label": ".Expunge()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L185", + "_origin": "ast", + "id": "imapclient_client_expunge", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".expunge()" + }, + { + "label": ".command()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L193", + "_origin": "ast", + "id": "imapclient_client_command", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".command()" + }, + { + "label": ".simpleCommand()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L213", + "_origin": "ast", + "id": "imapclient_client_simplecommand", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".simplecommand()" + }, + { + "label": ".readLine()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L232", + "_origin": "ast", + "id": "imapclient_client_readline", + "community": 13, + "community_name": "IMAP Client", + "norm_label": ".readline()" + }, + { + "label": "parseFetchLines()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L262", + "_origin": "ast", + "id": "internal_imapclient_client_parsefetchlines", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "parsefetchlines()" + }, + { + "label": "quote()", + "file_type": "code", + "source_file": "internal/imapclient/client.go", + "source_location": "L291", + "_origin": "ast", + "id": "internal_imapclient_client_quote", + "community": 13, + "community_name": "IMAP Client", + "norm_label": "quote()" + }, + { + "label": "jmap.go", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_jmap_jmap", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "jmap.go" + }, + { + "label": "Handler", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L34", + "_origin": "ast", + "id": "jmap_handler", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "handler" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_jmap_jmap_go_db", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "db" + }, + { + "label": "NewHandler()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L40", + "_origin": "ast", + "id": "internal_jmap_jmap_newhandler", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "newhandler()" + }, + { + "label": ".RegisterRoutes()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L44", + "_origin": "ast", + "id": "jmap_handler_registerroutes", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".registerroutes()" + }, + { + "label": "ServeMux", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_jmap_jmap_go_servemux", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "servemux" + }, + { + "label": ".session()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L51", + "_origin": "ast", + "id": "jmap_handler_session", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".session()" + }, + { + "label": "ResponseWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_jmap_jmap_go_responsewriter", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "responsewriter" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_jmap_jmap_go_request", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "request" + }, + { + "label": ".authenticate()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L98", + "_origin": "ast", + "id": "jmap_handler_authenticate", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".authenticate()" + }, + { + "label": "request", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L108", + "_origin": "ast", + "id": "jmap_request", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "request" + }, + { + "label": "response", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L113", + "_origin": "ast", + "id": "jmap_response", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "response" + }, + { + "label": ".api()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L118", + "_origin": "ast", + "id": "jmap_handler_api", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".api()" + }, + { + "label": "methodResult", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L151", + "_origin": "ast", + "id": "jmap_methodresult", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "methodresult" + }, + { + "label": ".dispatch()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L156", + "_origin": "ast", + "id": "jmap_handler_dispatch", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".dispatch()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_jmap_jmap_go_context", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "context" + }, + { + "label": ".mailboxGet()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L177", + "_origin": "ast", + "id": "jmap_handler_mailboxget", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".mailboxget()" + }, + { + "label": "jmapRole()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L202", + "_origin": "ast", + "id": "internal_jmap_jmap_jmaprole", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "jmaprole()" + }, + { + "label": ".emailQuery()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L221", + "_origin": "ast", + "id": "jmap_handler_emailquery", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".emailquery()" + }, + { + "label": ".emailGet()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L248", + "_origin": "ast", + "id": "jmap_handler_emailget", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": ".emailget()" + }, + { + "label": "splitCompositeID()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L282", + "_origin": "ast", + "id": "internal_jmap_jmap_splitcompositeid", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "splitcompositeid()" + }, + { + "label": "truncatePreview()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L290", + "_origin": "ast", + "id": "internal_jmap_jmap_truncatepreview", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "truncatepreview()" + }, + { + "label": "writeJSON()", + "file_type": "code", + "source_file": "internal/jmap/jmap.go", + "source_location": "L302", + "_origin": "ast", + "id": "internal_jmap_jmap_writejson", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "writejson()" + }, + { + "label": "maildir.go", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_mailstore_maildir", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "maildir.go" + }, + { + "label": "Store", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L26", + "_origin": "ast", + "id": "mailstore_store", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "store" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_mailstore_maildir_go_db", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "db" + }, + { + "label": "New()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L32", + "_origin": "ast", + "id": "internal_mailstore_maildir_new", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "new()" + }, + { + "label": ".Deliver()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L39", + "_origin": "ast", + "id": "mailstore_store_deliver", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".deliver()" + }, + { + "label": ".Read()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L93", + "_origin": "ast", + "id": "mailstore_store_read", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".read()" + }, + { + "label": ".ensureMailboxDirs()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L107", + "_origin": "ast", + "id": "mailstore_store_ensuremailboxdirs", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".ensuremailboxdirs()" + }, + { + "label": ".mailboxDir()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L117", + "_origin": "ast", + "id": "mailstore_store_mailboxdir", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".mailboxdir()" + }, + { + "label": "sanitizePathComponent()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L125", + "_origin": "ast", + "id": "internal_mailstore_maildir_sanitizepathcomponent", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "sanitizepathcomponent()" + }, + { + "label": "maildirFilename()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L139", + "_origin": "ast", + "id": "internal_mailstore_maildir_maildirfilename", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "maildirfilename()" + }, + { + "label": "messageIDFromFilename()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L145", + "_origin": "ast", + "id": "internal_mailstore_maildir_messageidfromfilename", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "messageidfromfilename()" + }, + { + "label": ".WriteQueueFile()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L159", + "_origin": "ast", + "id": "mailstore_store_writequeuefile", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".writequeuefile()" + }, + { + "label": ".DeleteQueueFile()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L188", + "_origin": "ast", + "id": "mailstore_store_deletequeuefile", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".deletequeuefile()" + }, + { + "label": ".WriteQuarantineFile()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L197", + "_origin": "ast", + "id": "mailstore_store_writequarantinefile", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".writequarantinefile()" + }, + { + "label": ".ReadQuarantineFile()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L225", + "_origin": "ast", + "id": "mailstore_store_readquarantinefile", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".readquarantinefile()" + }, + { + "label": ".ReadAt()", + "file_type": "code", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L235", + "_origin": "ast", + "id": "mailstore_store_readat", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": ".readat()" + }, + { + "label": "Writer", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_mailstore_maildir_go_writer", + "community": 2, + "community_name": "CalDAV/CardDAV Handlers", + "norm_label": "writer" + }, + { + "label": "managesieve/server.go", + "file_type": "code", + "source_file": "internal/managesieve/server.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_managesieve_server", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "managesieve/server.go" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "internal/managesieve/server.go", + "source_location": "L23", + "_origin": "ast", + "id": "managesieve_server", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "server" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_server_go_db", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "db" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_server_go_config", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "config" + }, + { + "label": "Listener", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_server_go_listener", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "listener" + }, + { + "label": "WaitGroup", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_server_go_waitgroup", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "waitgroup" + }, + { + "label": "NewServer()", + "file_type": "code", + "source_file": "internal/managesieve/server.go", + "source_location": "L33", + "_origin": "ast", + "id": "internal_managesieve_server_newserver", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "newserver()" + }, + { + "label": ".ListenAndServe()", + "file_type": "code", + "source_file": "internal/managesieve/server.go", + "source_location": "L37", + "_origin": "ast", + "id": "managesieve_server_listenandserve", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".listenandserve()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_server_go_context", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "context" + }, + { + "label": ".acceptLoop()", + "file_type": "code", + "source_file": "internal/managesieve/server.go", + "source_location": "L55", + "_origin": "ast", + "id": "managesieve_server_acceptloop", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".acceptloop()" + }, + { + "label": ".Shutdown()", + "file_type": "code", + "source_file": "internal/managesieve/server.go", + "source_location": "L75", + "_origin": "ast", + "id": "managesieve_server_shutdown", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".shutdown()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_server_go_duration", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "duration" + }, + { + "label": "managesieve/session.go", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_managesieve_session", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "managesieve/session.go" + }, + { + "label": "session", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L22", + "_origin": "ast", + "id": "managesieve_session", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "session" + }, + { + "label": "Conn", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_session_go_conn", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "conn" + }, + { + "label": "ReadWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_session_go_readwriter", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "readwriter" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_session_go_server", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "server" + }, + { + "label": "newSession()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L30", + "_origin": "ast", + "id": "internal_managesieve_session_newsession", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "newsession()" + }, + { + "label": ".run()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L40", + "_origin": "ast", + "id": "managesieve_session_run", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_managesieve_session_go_context", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "context" + }, + { + "label": ".sendCapabilities()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L65", + "_origin": "ast", + "id": "managesieve_session_sendcapabilities", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".sendcapabilities()" + }, + { + "label": ".dispatch()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L75", + "_origin": "ast", + "id": "managesieve_session_dispatch", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".dispatch()" + }, + { + "label": ".cmdStartTLS()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L105", + "_origin": "ast", + "id": "managesieve_session_cmdstarttls", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmdstarttls()" + }, + { + "label": ".cmdAuthenticate()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L123", + "_origin": "ast", + "id": "managesieve_session_cmdauthenticate", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmdauthenticate()" + }, + { + "label": ".requireAuth()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L168", + "_origin": "ast", + "id": "managesieve_session_requireauth", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".requireauth()" + }, + { + "label": ".cmdPutScript()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L177", + "_origin": "ast", + "id": "managesieve_session_cmdputscript", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmdputscript()" + }, + { + "label": ".cmdGetScript()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L208", + "_origin": "ast", + "id": "managesieve_session_cmdgetscript", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmdgetscript()" + }, + { + "label": ".cmdListScripts()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L225", + "_origin": "ast", + "id": "managesieve_session_cmdlistscripts", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmdlistscripts()" + }, + { + "label": ".cmdSetActive()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L244", + "_origin": "ast", + "id": "managesieve_session_cmdsetactive", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmdsetactive()" + }, + { + "label": ".cmdDeleteScript()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L262", + "_origin": "ast", + "id": "managesieve_session_cmddeletescript", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".cmddeletescript()" + }, + { + "label": ".writeLine()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L276", + "_origin": "ast", + "id": "managesieve_session_writeline", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".writeline()" + }, + { + "label": ".readLine()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L281", + "_origin": "ast", + "id": "managesieve_session_readline", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".readline()" + }, + { + "label": ".readLiteralFromRemainder()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L292", + "_origin": "ast", + "id": "managesieve_session_readliteralfromremainder", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": ".readliteralfromremainder()" + }, + { + "label": "splitVerb()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L312", + "_origin": "ast", + "id": "internal_managesieve_session_splitverb", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "splitverb()" + }, + { + "label": "splitQuotedArgs()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L324", + "_origin": "ast", + "id": "internal_managesieve_session_splitquotedargs", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "splitquotedargs()" + }, + { + "label": "escapeQuoted()", + "file_type": "code", + "source_file": "internal/managesieve/session.go", + "source_location": "L357", + "_origin": "ast", + "id": "internal_managesieve_session_escapequoted", + "community": 8, + "community_name": "IMAP Server Loop", + "norm_label": "escapequoted()" + }, + { + "label": "oauth2.go", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_oauth2_oauth2", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "oauth2.go" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L24", + "_origin": "ast", + "id": "oauth2_config", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "config" + }, + { + "label": "WellKnownEndpoints()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L38", + "_origin": "ast", + "id": "internal_oauth2_oauth2_wellknownendpoints", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "wellknownendpoints()" + }, + { + "label": "Token", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L51", + "_origin": "ast", + "id": "oauth2_token", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "token" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_oauth2_oauth2_go_time", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "time" + }, + { + "label": "tokenResponse", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L58", + "_origin": "ast", + "id": "oauth2_tokenresponse", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "tokenresponse" + }, + { + "label": ".BuildAuthURL()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L71", + "_origin": "ast", + "id": "oauth2_config_buildauthurl", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": ".buildauthurl()" + }, + { + "label": ".ExchangeCode()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L85", + "_origin": "ast", + "id": "oauth2_config_exchangecode", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": ".exchangecode()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_oauth2_oauth2_go_context", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "context" + }, + { + "label": ".RefreshToken()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L96", + "_origin": "ast", + "id": "oauth2_config_refreshtoken", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": ".refreshtoken()" + }, + { + "label": ".doTokenRequest()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L112", + "_origin": "ast", + "id": "oauth2_config_dotokenrequest", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": ".dotokenrequest()" + }, + { + "label": "Values", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "values", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "values" + }, + { + "label": "truncate()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L150", + "_origin": "ast", + "id": "internal_oauth2_oauth2_truncate", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "truncate()" + }, + { + "label": "XOAUTH2SASLString()", + "file_type": "code", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L161", + "_origin": "ast", + "id": "internal_oauth2_oauth2_xoauth2saslstring", + "community": 20, + "community_name": "OAuth2 Config", + "norm_label": "xoauth2saslstring()" + }, + { + "label": "pipeline.go", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_pipeline", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "pipeline.go" + }, + { + "label": "MailContext", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L22", + "_origin": "ast", + "id": "pipeline_mailcontext", + "community": 44, + "community_name": "Mail Context Domain Helpers", + "norm_label": "mailcontext" + }, + { + "label": "IP", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_pipeline_go_ip", + "community": 44, + "community_name": "Mail Context Domain Helpers", + "norm_label": "ip" + }, + { + "label": ".ParsedMessage()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L41", + "_origin": "ast", + "id": "pipeline_mailcontext_parsedmessage", + "community": 23, + "community_name": "Quarantine & Message Queries", + "norm_label": ".parsedmessage()" + }, + { + "label": ".RcptDomain()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L50", + "_origin": "ast", + "id": "pipeline_mailcontext_rcptdomain", + "community": 44, + "community_name": "Mail Context Domain Helpers", + "norm_label": ".rcptdomain()" + }, + { + "label": ".MailFromDomain()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L55", + "_origin": "ast", + "id": "pipeline_mailcontext_mailfromdomain", + "community": 44, + "community_name": "Mail Context Domain Helpers", + "norm_label": ".mailfromdomain()" + }, + { + "label": "domainOf()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L59", + "_origin": "ast", + "id": "internal_pipeline_pipeline_domainof", + "community": 44, + "community_name": "Mail Context Domain Helpers", + "norm_label": "domainof()" + }, + { + "label": "StageResult", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L68", + "_origin": "ast", + "id": "pipeline_stageresult", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": "stageresult" + }, + { + "label": "Stage", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L78", + "_origin": "ast", + "id": "pipeline_stage", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "stage" + }, + { + "label": "Orchestrator", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L84", + "_origin": "ast", + "id": "pipeline_orchestrator", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "orchestrator" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_pipeline_go_config", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "config" + }, + { + "label": "NewOrchestrator()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L89", + "_origin": "ast", + "id": "internal_pipeline_pipeline_neworchestrator", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "neworchestrator()" + }, + { + "label": "DefaultStages()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L96", + "_origin": "ast", + "id": "internal_pipeline_pipeline_defaultstages", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "defaultstages()" + }, + { + "label": "StagesFromConfig()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L112", + "_origin": "ast", + "id": "internal_pipeline_pipeline_stagesfromconfig", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "stagesfromconfig()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L137", + "_origin": "ast", + "id": "pipeline_orchestrator_run", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_pipeline_go_context", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "context" + }, + { + "label": "verdictFor()", + "file_type": "code", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L152", + "_origin": "ast", + "id": "internal_pipeline_pipeline_verdictfor", + "community": 28, + "community_name": "Spam Pipeline Orchestrator", + "norm_label": "verdictfor()" + }, + { + "label": "stage_clamav.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_clamav", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": "stage_clamav.go" + }, + { + "label": "ClamAVStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L21", + "_origin": "ast", + "id": "pipeline_clamavstage", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": "clamavstage" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_clamav_go_duration", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": "duration" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L26", + "_origin": "ast", + "id": "pipeline_clamavstage_name", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": ".name()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L28", + "_origin": "ast", + "id": "pipeline_clamavstage_run", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_clamav_go_context", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": "context" + }, + { + "label": ".scan()", + "file_type": "code", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L55", + "_origin": "ast", + "id": "pipeline_clamavstage_scan", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": ".scan()" + }, + { + "label": "parseClamAddr()", + "file_type": "code", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L118", + "_origin": "ast", + "id": "internal_pipeline_stage_clamav_parseclamaddr", + "community": 34, + "community_name": "ClamAV Stage", + "norm_label": "parseclamaddr()" + }, + { + "label": "stage_dkim.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_dkim", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "stage_dkim.go" + }, + { + "label": "DKIMStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L13", + "_origin": "ast", + "id": "pipeline_dkimstage", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "dkimstage" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L15", + "_origin": "ast", + "id": "pipeline_dkimstage_name", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": ".name()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L17", + "_origin": "ast", + "id": "pipeline_dkimstage_run", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_dkim_go_context", + "community": 14, + "community_name": "DKIM Key Signing", + "norm_label": "context" + }, + { + "label": "stage_dmarc.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_dmarc", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": "stage_dmarc.go" + }, + { + "label": "DMARCStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L12", + "_origin": "ast", + "id": "pipeline_dmarcstage", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": "dmarcstage" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L14", + "_origin": "ast", + "id": "pipeline_dmarcstage_name", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": ".name()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L16", + "_origin": "ast", + "id": "pipeline_dmarcstage_run", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_dmarc_go_context", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": "context" + }, + { + "label": "extractDomainFromHeader()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L76", + "_origin": "ast", + "id": "internal_pipeline_stage_dmarc_extractdomainfromheader", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": "extractdomainfromheader()" + }, + { + "label": "orgDomain()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L98", + "_origin": "ast", + "id": "internal_pipeline_stage_dmarc_orgdomain", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": "orgdomain()" + }, + { + "label": "dmarcTag()", + "file_type": "code", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L106", + "_origin": "ast", + "id": "internal_pipeline_stage_dmarc_dmarctag", + "community": 35, + "community_name": "DMARC Stage", + "norm_label": "dmarctag()" + }, + { + "label": "stage_headers.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_headers", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": "stage_headers.go" + }, + { + "label": "HeaderStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L11", + "_origin": "ast", + "id": "pipeline_headerstage", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": "headerstage" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L13", + "_origin": "ast", + "id": "pipeline_headerstage_name", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": ".name()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L15", + "_origin": "ast", + "id": "pipeline_headerstage_run", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_headers_go_context", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": "context" + }, + { + "label": "stage_llm.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_llm", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "stage_llm.go" + }, + { + "label": "LLMStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L28", + "_origin": "ast", + "id": "pipeline_llmstage", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "llmstage" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_llm_go_duration", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "duration" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L34", + "_origin": "ast", + "id": "pipeline_llmstage_name", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": ".name()" + }, + { + "label": "chatCompletionRequest", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L41", + "_origin": "ast", + "id": "pipeline_chatcompletionrequest", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "chatcompletionrequest" + }, + { + "label": "chatMessage", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L46", + "_origin": "ast", + "id": "pipeline_chatmessage", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "chatmessage" + }, + { + "label": "chatCompletionResponse", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L51", + "_origin": "ast", + "id": "pipeline_chatcompletionresponse", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "chatcompletionresponse" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L62", + "_origin": "ast", + "id": "pipeline_llmstage_run", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_llm_go_context", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "context" + }, + { + "label": ".classify()", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L93", + "_origin": "ast", + "id": "pipeline_llmstage_classify", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": ".classify()" + }, + { + "label": "extractLeadingDigits()", + "file_type": "code", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L149", + "_origin": "ast", + "id": "internal_pipeline_stage_llm_extractleadingdigits", + "community": 25, + "community_name": "LLM Spam Stage", + "norm_label": "extractleadingdigits()" + }, + { + "label": "stage_rspamd.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_rspamd", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": "stage_rspamd.go" + }, + { + "label": "RspamdStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L19", + "_origin": "ast", + "id": "pipeline_rspamdstage", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": "rspamdstage" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_rspamd_go_duration", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": "duration" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L24", + "_origin": "ast", + "id": "pipeline_rspamdstage_name", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": ".name()" + }, + { + "label": "rspamdResponse", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L26", + "_origin": "ast", + "id": "pipeline_rspamdresponse", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": "rspamdresponse" + }, + { + "label": "rspamdSymbol", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L33", + "_origin": "ast", + "id": "pipeline_rspamdsymbol", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": "rspamdsymbol" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L39", + "_origin": "ast", + "id": "pipeline_rspamdstage_run", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_rspamd_go_context", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": "context" + }, + { + "label": ".check()", + "file_type": "code", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L69", + "_origin": "ast", + "id": "pipeline_rspamdstage_check", + "community": 32, + "community_name": "Rspamd Stage", + "norm_label": ".check()" + }, + { + "label": "stage_spf.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_spf", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "stage_spf.go" + }, + { + "label": "SPFStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L12", + "_origin": "ast", + "id": "pipeline_spfstage", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "spfstage" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L14", + "_origin": "ast", + "id": "pipeline_spfstage_name", + "community": 29, + "community_name": "SPF Stage", + "norm_label": ".name()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L16", + "_origin": "ast", + "id": "pipeline_spfstage_run", + "community": 29, + "community_name": "SPF Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_spf_go_context", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "context" + }, + { + "label": "spfOutcome", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L31", + "_origin": "ast", + "id": "pipeline_spfoutcome", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "spfoutcome" + }, + { + "label": "checkSPF()", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L36", + "_origin": "ast", + "id": "internal_pipeline_stage_spf_checkspf", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "checkspf()" + }, + { + "label": "IP", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_spf_go_ip", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "ip" + }, + { + "label": "evaluateSPF()", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L73", + "_origin": "ast", + "id": "internal_pipeline_stage_spf_evaluatespf", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "evaluatespf()" + }, + { + "label": "matchCIDR()", + "file_type": "code", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L153", + "_origin": "ast", + "id": "internal_pipeline_stage_spf_matchcidr", + "community": 29, + "community_name": "SPF Stage", + "norm_label": "matchcidr()" + }, + { + "label": "stage_url.go", + "file_type": "code", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pipeline_stage_url", + "community": 40, + "community_name": "URL Extraction Stage", + "norm_label": "stage_url.go" + }, + { + "label": "URLStage", + "file_type": "code", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L12", + "_origin": "ast", + "id": "pipeline_urlstage", + "community": 40, + "community_name": "URL Extraction Stage", + "norm_label": "urlstage" + }, + { + "label": ".Name()", + "file_type": "code", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L14", + "_origin": "ast", + "id": "pipeline_urlstage_name", + "community": 40, + "community_name": "URL Extraction Stage", + "norm_label": ".name()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L26", + "_origin": "ast", + "id": "pipeline_urlstage_run", + "community": 40, + "community_name": "URL Extraction Stage", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pipeline_stage_url_go_context", + "community": 40, + "community_name": "URL Extraction Stage", + "norm_label": "context" + }, + { + "label": "dedupe()", + "file_type": "code", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L89", + "_origin": "ast", + "id": "internal_pipeline_stage_url_dedupe", + "community": 40, + "community_name": "URL Extraction Stage", + "norm_label": "dedupe()" + }, + { + "label": "pop3.go", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_pop3_pop3", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "pop3.go" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L30", + "_origin": "ast", + "id": "pop3_server", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "server" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_db", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "db" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_config", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "config" + }, + { + "label": "Listener", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_listener", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "listener" + }, + { + "label": "WaitGroup", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_waitgroup", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "waitgroup" + }, + { + "label": "NewServer()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L41", + "_origin": "ast", + "id": "internal_pop3_pop3_newserver", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "newserver()" + }, + { + "label": ".ListenAndServe()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L45", + "_origin": "ast", + "id": "pop3_server_listenandserve", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".listenandserve()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_context", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "context" + }, + { + "label": ".acceptLoop()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L77", + "_origin": "ast", + "id": "pop3_server_acceptloop", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".acceptloop()" + }, + { + "label": ".Shutdown()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L98", + "_origin": "ast", + "id": "pop3_server_shutdown", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".shutdown()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_duration", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "duration" + }, + { + "label": ".closeAll()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L113", + "_origin": "ast", + "id": "pop3_server_closeall", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".closeall()" + }, + { + "label": "pop3State", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L122", + "_origin": "ast", + "id": "pop3_pop3state", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "pop3state" + }, + { + "label": "session", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L130", + "_origin": "ast", + "id": "pop3_session", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "session" + }, + { + "label": "Conn", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_conn", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "conn" + }, + { + "label": "ReadWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_pop3_pop3_go_readwriter", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "readwriter" + }, + { + "label": "newSession()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L147", + "_origin": "ast", + "id": "internal_pop3_pop3_newsession", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": "newsession()" + }, + { + "label": ".run()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L159", + "_origin": "ast", + "id": "pop3_session_run", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".run()" + }, + { + "label": ".dispatch()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L186", + "_origin": "ast", + "id": "pop3_session_dispatch", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".dispatch()" + }, + { + "label": ".reply()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L225", + "_origin": "ast", + "id": "pop3_session_reply", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".reply()" + }, + { + "label": ".cmdUser()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L240", + "_origin": "ast", + "id": "pop3_session_cmduser", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmduser()" + }, + { + "label": ".cmdPass()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L249", + "_origin": "ast", + "id": "pop3_session_cmdpass", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmdpass()" + }, + { + "label": ".cmdStat()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L275", + "_origin": "ast", + "id": "pop3_session_cmdstat", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmdstat()" + }, + { + "label": ".cmdList()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L291", + "_origin": "ast", + "id": "pop3_session_cmdlist", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmdlist()" + }, + { + "label": ".cmdUIDL()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L316", + "_origin": "ast", + "id": "pop3_session_cmduidl", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmduidl()" + }, + { + "label": ".cmdRetr()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L341", + "_origin": "ast", + "id": "pop3_session_cmdretr", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmdretr()" + }, + { + "label": ".cmdTop()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L359", + "_origin": "ast", + "id": "pop3_session_cmdtop", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmdtop()" + }, + { + "label": ".cmdDele()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L404", + "_origin": "ast", + "id": "pop3_session_cmddele", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmddele()" + }, + { + "label": ".cmdRset()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L416", + "_origin": "ast", + "id": "pop3_session_cmdrset", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".cmdrset()" + }, + { + "label": ".commitDeletes()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L427", + "_origin": "ast", + "id": "pop3_session_commitdeletes", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".commitdeletes()" + }, + { + "label": ".requireTransaction()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L441", + "_origin": "ast", + "id": "pop3_session_requiretransaction", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".requiretransaction()" + }, + { + "label": ".validMessageNum()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L449", + "_origin": "ast", + "id": "pop3_session_validmessagenum", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".validmessagenum()" + }, + { + "label": ".liveCount()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L462", + "_origin": "ast", + "id": "pop3_session_livecount", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".livecount()" + }, + { + "label": ".writeDotStuffed()", + "file_type": "code", + "source_file": "internal/pop3/pop3.go", + "source_location": "L475", + "_origin": "ast", + "id": "pop3_session_writedotstuffed", + "community": 9, + "community_name": "POP3 Server Loop", + "norm_label": ".writedotstuffed()" + }, + { + "label": "queue.go", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_queue_queue", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "queue.go" + }, + { + "label": "Deliverer", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L29", + "_origin": "ast", + "id": "queue_deliverer", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "deliverer" + }, + { + "label": "KeyLookup", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L37", + "_origin": "ast", + "id": "queue_keylookup", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "keylookup" + }, + { + "label": "Worker", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L40", + "_origin": "ast", + "id": "queue_worker", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "worker" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_queue_queue_go_db", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "db" + }, + { + "label": "NewWorker()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L48", + "_origin": "ast", + "id": "internal_queue_queue_newworker", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "newworker()" + }, + { + "label": ".WithDeliverer()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L58", + "_origin": "ast", + "id": "queue_worker_withdeliverer", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".withdeliverer()" + }, + { + "label": ".WithKeyLookup()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L67", + "_origin": "ast", + "id": "queue_worker_withkeylookup", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".withkeylookup()" + }, + { + "label": ".Run()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L73", + "_origin": "ast", + "id": "queue_worker_run", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".run()" + }, + { + "label": ".Stop()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L90", + "_origin": "ast", + "id": "queue_worker_stop", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".stop()" + }, + { + "label": ".ProcessOnce()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L96", + "_origin": "ast", + "id": "queue_worker_processonce", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".processonce()" + }, + { + "label": ".attemptDelivery()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L117", + "_origin": "ast", + "id": "queue_worker_attemptdelivery", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".attemptdelivery()" + }, + { + "label": ".scheduleRetry()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L161", + "_origin": "ast", + "id": "queue_worker_scheduleretry", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".scheduleretry()" + }, + { + "label": "backoffDuration()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L172", + "_origin": "ast", + "id": "internal_queue_queue_backoffduration", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "backoffduration()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_queue_queue_go_duration", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "duration" + }, + { + "label": ".bounce()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L188", + "_origin": "ast", + "id": "queue_worker_bounce", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".bounce()" + }, + { + "label": "domainOf()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L220", + "_origin": "ast", + "id": "internal_queue_queue_domainof", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "domainof()" + }, + { + "label": "isPermanentError()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L231", + "_origin": "ast", + "id": "internal_queue_queue_ispermanenterror", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "ispermanenterror()" + }, + { + "label": "MXDeliverer", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L252", + "_origin": "ast", + "id": "queue_mxdeliverer", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "mxdeliverer" + }, + { + "label": ".Deliver()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L256", + "_origin": "ast", + "id": "queue_mxdeliverer_deliver", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".deliver()" + }, + { + "label": ".deliverToHost()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L274", + "_origin": "ast", + "id": "queue_mxdeliverer_delivertohost", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": ".delivertohost()" + }, + { + "label": "lookupMXHosts()", + "file_type": "code", + "source_file": "internal/queue/queue.go", + "source_location": "L320", + "_origin": "ast", + "id": "internal_queue_queue_lookupmxhosts", + "community": 15, + "community_name": "Outbound Delivery Queue", + "norm_label": "lookupmxhosts()" + }, + { + "label": "http.go", + "file_type": "code", + "source_file": "internal/ratelimit/http.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_ratelimit_http", + "community": 39, + "community_name": "HTTP Middleware", + "norm_label": "http.go" + }, + { + "label": "Limiter", + "file_type": "code", + "source_file": "internal/ratelimit/http.go", + "source_location": "L14", + "_origin": "ast", + "id": "internal_ratelimit_http_go_ratelimit_limiter", + "community": 39, + "community_name": "HTTP Middleware", + "norm_label": "limiter" + }, + { + "label": ".HTTPMiddleware()", + "file_type": "code", + "source_file": "internal/ratelimit/http.go", + "source_location": "L14", + "_origin": "ast", + "id": "ratelimit_limiter_httpmiddleware", + "community": 39, + "community_name": "HTTP Middleware", + "norm_label": ".httpmiddleware()" + }, + { + "label": "Handler", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "handler", + "community": 39, + "community_name": "HTTP Middleware", + "norm_label": "handler" + }, + { + "label": "clientIP()", + "file_type": "code", + "source_file": "internal/ratelimit/http.go", + "source_location": "L26", + "_origin": "ast", + "id": "internal_ratelimit_http_clientip", + "community": 39, + "community_name": "HTTP Middleware", + "norm_label": "clientip()" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_ratelimit_http_go_request", + "community": 39, + "community_name": "HTTP Middleware", + "norm_label": "request" + }, + { + "label": "ratelimit.go", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_ratelimit_ratelimit", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": "ratelimit.go" + }, + { + "label": "bucket", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L14", + "_origin": "ast", + "id": "ratelimit_bucket", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": "bucket" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_ratelimit_ratelimit_go_time", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": "time" + }, + { + "label": "Limiter", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L24", + "_origin": "ast", + "id": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": "limiter" + }, + { + "label": "Mutex", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_ratelimit_ratelimit_go_mutex", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": "mutex" + }, + { + "label": "New()", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L36", + "_origin": "ast", + "id": "internal_ratelimit_ratelimit_new", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": "new()" + }, + { + "label": ".Allow()", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L50", + "_origin": "ast", + "id": "ratelimit_limiter_allow", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": ".allow()" + }, + { + "label": ".cleanupLoop()", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L84", + "_origin": "ast", + "id": "ratelimit_limiter_cleanuploop", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": ".cleanuploop()" + }, + { + "label": ".Stop()", + "file_type": "code", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L105", + "_origin": "ast", + "id": "ratelimit_limiter_stop", + "community": 33, + "community_name": "Per-IP Rate Limiter", + "norm_label": ".stop()" + }, + { + "label": "interp.go", + "file_type": "code", + "source_file": "internal/sieve/interp.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_sieve_interp", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "interp.go" + }, + { + "label": "Result", + "file_type": "code", + "source_file": "internal/sieve/interp.go", + "source_location": "L6", + "_origin": "ast", + "id": "sieve_result", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "result" + }, + { + "label": "Execute()", + "file_type": "code", + "source_file": "internal/sieve/interp.go", + "source_location": "L29", + "_origin": "ast", + "id": "internal_sieve_interp_execute", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "execute()" + }, + { + "label": "execStatements()", + "file_type": "code", + "source_file": "internal/sieve/interp.go", + "source_location": "L36", + "_origin": "ast", + "id": "internal_sieve_interp_execstatements", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "execstatements()" + }, + { + "label": "evalTest()", + "file_type": "code", + "source_file": "internal/sieve/interp.go", + "source_location": "L78", + "_origin": "ast", + "id": "internal_sieve_interp_evaltest", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "evaltest()" + }, + { + "label": "lookupHeader()", + "file_type": "code", + "source_file": "internal/sieve/interp.go", + "source_location": "L97", + "_origin": "ast", + "id": "internal_sieve_interp_lookupheader", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "lookupheader()" + }, + { + "label": "lexer.go", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_sieve_lexer", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "lexer.go" + }, + { + "label": "tokenKind", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L18", + "_origin": "ast", + "id": "sieve_tokenkind", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "tokenkind" + }, + { + "label": "token", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L30", + "_origin": "ast", + "id": "sieve_token", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "token" + }, + { + "label": "lexer", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L35", + "_origin": "ast", + "id": "sieve_lexer", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "lexer" + }, + { + "label": "newLexer()", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L40", + "_origin": "ast", + "id": "internal_sieve_lexer_newlexer", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "newlexer()" + }, + { + "label": ".next()", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L44", + "_origin": "ast", + "id": "sieve_lexer_next", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".next()" + }, + { + "label": ".skipWhitespaceAndComments()", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L72", + "_origin": "ast", + "id": "sieve_lexer_skipwhitespaceandcomments", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".skipwhitespaceandcomments()" + }, + { + "label": ".readString()", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L99", + "_origin": "ast", + "id": "sieve_lexer_readstring", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".readstring()" + }, + { + "label": ".readTag()", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L116", + "_origin": "ast", + "id": "sieve_lexer_readtag", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".readtag()" + }, + { + "label": ".readIdent()", + "file_type": "code", + "source_file": "internal/sieve/lexer.go", + "source_location": "L125", + "_origin": "ast", + "id": "sieve_lexer_readident", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".readident()" + }, + { + "label": "sieve/parser.go", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_sieve_parser", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "sieve/parser.go" + }, + { + "label": "Script", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L7", + "_origin": "ast", + "id": "sieve_script", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "script" + }, + { + "label": "Statement", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L12", + "_origin": "ast", + "id": "sieve_statement", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "statement" + }, + { + "label": "Action", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L14", + "_origin": "ast", + "id": "sieve_action", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "action" + }, + { + "label": ".isStatement()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L19", + "_origin": "ast", + "id": "sieve_action_isstatement", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".isstatement()" + }, + { + "label": "IfStatement", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L21", + "_origin": "ast", + "id": "sieve_ifstatement", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "ifstatement" + }, + { + "label": ".isStatement()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L29", + "_origin": "ast", + "id": "sieve_ifstatement_isstatement", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".isstatement()" + }, + { + "label": "ElseIf", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L31", + "_origin": "ast", + "id": "sieve_elseif", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "elseif" + }, + { + "label": "Test", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L38", + "_origin": "ast", + "id": "sieve_test", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "test" + }, + { + "label": "parser", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L47", + "_origin": "ast", + "id": "sieve_parser", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "parser" + }, + { + "label": "Parse()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L52", + "_origin": "ast", + "id": "internal_sieve_parser_parse", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "parse()" + }, + { + "label": ".advance()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L69", + "_origin": "ast", + "id": "sieve_parser_advance", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".advance()" + }, + { + "label": ".expect()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L78", + "_origin": "ast", + "id": "sieve_parser_expect", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".expect()" + }, + { + "label": ".parseStatement()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L89", + "_origin": "ast", + "id": "sieve_parser_parsestatement", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".parsestatement()" + }, + { + "label": ".parseIf()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L123", + "_origin": "ast", + "id": "sieve_parser_parseif", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".parseif()" + }, + { + "label": ".parseTest()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L168", + "_origin": "ast", + "id": "sieve_parser_parsetest", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".parsetest()" + }, + { + "label": ".parseBlock()", + "file_type": "code", + "source_file": "internal/sieve/parser.go", + "source_location": "L210", + "_origin": "ast", + "id": "sieve_parser_parseblock", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": ".parseblock()" + }, + { + "label": "sieve_fuzz_test.go", + "file_type": "code", + "source_file": "internal/sieve/sieve_fuzz_test.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_sieve_sieve_fuzz_test", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "sieve_fuzz_test.go" + }, + { + "label": "FuzzParse()", + "file_type": "code", + "source_file": "internal/sieve/sieve_fuzz_test.go", + "source_location": "L5", + "_origin": "ast", + "id": "internal_sieve_sieve_fuzz_test_fuzzparse", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "fuzzparse()" + }, + { + "label": "F", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_sieve_sieve_fuzz_test_go_f", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "f" + }, + { + "label": "smtp/auth.go", + "file_type": "code", + "source_file": "internal/smtp/auth.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_smtp_auth", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "smtp/auth.go" + }, + { + "label": "authenticate()", + "file_type": "code", + "source_file": "internal/smtp/auth.go", + "source_location": "L9", + "_origin": "ast", + "id": "internal_smtp_auth_authenticate", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "authenticate()" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_auth_go_db", + "community": 10, + "community_name": "JMAP Auth & Sessions", + "norm_label": "db" + }, + { + "label": "smtp/server.go", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_smtp_server", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "smtp/server.go" + }, + { + "label": "Kind", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L34", + "_origin": "ast", + "id": "smtp_kind", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "kind" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L42", + "_origin": "ast", + "id": "smtp_server", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "server" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_config", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "config" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_db", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "db" + }, + { + "label": "Listener", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_listener", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "listener" + }, + { + "label": "WaitGroup", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_waitgroup", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "waitgroup" + }, + { + "label": "Limiter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_limiter", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "limiter" + }, + { + "label": "NewServer()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "_origin": "ast", + "id": "internal_smtp_server_newserver", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "newserver()" + }, + { + "label": ".ListenAndServe()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L74", + "_origin": "ast", + "id": "smtp_server_listenandserve", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": ".listenandserve()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_context", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "context" + }, + { + "label": ".acceptLoop()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L115", + "_origin": "ast", + "id": "smtp_server_acceptloop", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": ".acceptloop()" + }, + { + "label": ".handleConn()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L143", + "_origin": "ast", + "id": "smtp_server_handleconn", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": ".handleconn()" + }, + { + "label": "Conn", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_conn", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "conn" + }, + { + "label": ".Shutdown()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L165", + "_origin": "ast", + "id": "smtp_server_shutdown", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": ".shutdown()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_duration", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "duration" + }, + { + "label": ".closeAll()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L182", + "_origin": "ast", + "id": "smtp_server_closeall", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": ".closeall()" + }, + { + "label": "kindName()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L189", + "_origin": "ast", + "id": "internal_smtp_server_kindname", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "kindname()" + }, + { + "label": "connHost()", + "file_type": "code", + "source_file": "internal/smtp/server.go", + "source_location": "L206", + "_origin": "ast", + "id": "internal_smtp_server_connhost", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "connhost()" + }, + { + "label": "Addr", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_server_go_addr", + "community": 16, + "community_name": "SMTP Server Networking", + "norm_label": "addr" + }, + { + "label": "smtp/session.go", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_smtp_session", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "smtp/session.go" + }, + { + "label": "state", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L22", + "_origin": "ast", + "id": "smtp_state", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "state" + }, + { + "label": "session", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L31", + "_origin": "ast", + "id": "smtp_session", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "session" + }, + { + "label": "Conn", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_session_go_conn", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "conn" + }, + { + "label": "ReadWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_session_go_readwriter", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "readwriter" + }, + { + "label": "Server", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_session_go_server", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "server" + }, + { + "label": "IP", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_session_go_ip", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "ip" + }, + { + "label": "recipientTarget", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L48", + "_origin": "ast", + "id": "smtp_recipienttarget", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "recipienttarget" + }, + { + "label": ".isSubmissionKind()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L54", + "_origin": "ast", + "id": "smtp_session_issubmissionkind", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".issubmissionkind()" + }, + { + "label": ".run()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L58", + "_origin": "ast", + "id": "smtp_session_run", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".run()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_smtp_session_go_context", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "context" + }, + { + "label": ".handleCommand()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L92", + "_origin": "ast", + "id": "smtp_session_handlecommand", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handlecommand()" + }, + { + "label": ".handleHelo()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L132", + "_origin": "ast", + "id": "smtp_session_handlehelo", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handlehelo()" + }, + { + "label": ".handleStartTLS()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L164", + "_origin": "ast", + "id": "smtp_session_handlestarttls", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handlestarttls()" + }, + { + "label": ".handleAuth()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L187", + "_origin": "ast", + "id": "smtp_session_handleauth", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handleauth()" + }, + { + "label": ".readAuthPlain()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L239", + "_origin": "ast", + "id": "smtp_session_readauthplain", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".readauthplain()" + }, + { + "label": ".readAuthLogin()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L261", + "_origin": "ast", + "id": "smtp_session_readauthlogin", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".readauthlogin()" + }, + { + "label": ".handleMailFrom()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L285", + "_origin": "ast", + "id": "smtp_session_handlemailfrom", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handlemailfrom()" + }, + { + "label": ".handleRcptTo()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L312", + "_origin": "ast", + "id": "smtp_session_handlercptto", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handlercptto()" + }, + { + "label": ".handleData()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L379", + "_origin": "ast", + "id": "smtp_session_handledata", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".handledata()" + }, + { + "label": ".quarantineMessage()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L560", + "_origin": "ast", + "id": "smtp_session_quarantinemessage", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".quarantinemessage()" + }, + { + "label": "applySieve()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L589", + "_origin": "ast", + "id": "internal_smtp_session_applysieve", + "community": 6, + "community_name": "Sieve Filter Interpreter", + "norm_label": "applysieve()" + }, + { + "label": "extractHeaderMap()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L601", + "_origin": "ast", + "id": "internal_smtp_session_extractheadermap", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "extractheadermap()" + }, + { + "label": "injectSpamHeaders()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L624", + "_origin": "ast", + "id": "internal_smtp_session_injectspamheaders", + "community": 36, + "community_name": "Spam Header Injection Stage", + "norm_label": "injectspamheaders()" + }, + { + "label": ".reset()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L639", + "_origin": "ast", + "id": "smtp_session_reset", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".reset()" + }, + { + "label": ".writeLine()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L652", + "_origin": "ast", + "id": "smtp_session_writeline", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".writeline()" + }, + { + "label": ".readLine()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L658", + "_origin": "ast", + "id": "smtp_session_readline", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".readline()" + }, + { + "label": ".readDotStuffed()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L668", + "_origin": "ast", + "id": "smtp_session_readdotstuffed", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": ".readdotstuffed()" + }, + { + "label": "splitVerb()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L693", + "_origin": "ast", + "id": "internal_smtp_session_splitverb", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "splitverb()" + }, + { + "label": "parseMailCmdArg()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L708", + "_origin": "ast", + "id": "internal_smtp_session_parsemailcmdarg", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "parsemailcmdarg()" + }, + { + "label": "senderIPString()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L736", + "_origin": "ast", + "id": "internal_smtp_session_senderipstring", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "senderipstring()" + }, + { + "label": "extractSubject()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L743", + "_origin": "ast", + "id": "internal_smtp_session_extractsubject", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "extractsubject()" + }, + { + "label": "extractMessageID()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L747", + "_origin": "ast", + "id": "internal_smtp_session_extractmessageid", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "extractmessageid()" + }, + { + "label": "extractHeader()", + "file_type": "code", + "source_file": "internal/smtp/session.go", + "source_location": "L751", + "_origin": "ast", + "id": "internal_smtp_session_extractheader", + "community": 11, + "community_name": "SMTP Session Header Parsing", + "norm_label": "extractheader()" + }, + { + "label": "acme_manager.go", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "acme_manager.go" + }, + { + "label": "ACMEManager", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L26", + "_origin": "ast", + "id": "tlsutil_acmemanager", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "acmemanager" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_go_db", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "db" + }, + { + "label": "RWMutex", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_go_rwmutex", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "rwmutex" + }, + { + "label": "Certificate", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_go_certificate", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "certificate" + }, + { + "label": "NewACMEManager()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L37", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_newacmemanager", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "newacmemanager()" + }, + { + "label": ".TLSConfig()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L46", + "_origin": "ast", + "id": "tlsutil_acmemanager_tlsconfig", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".tlsconfig()" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_go_config", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "config" + }, + { + "label": ".CertificateFor()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L58", + "_origin": "ast", + "id": "tlsutil_acmemanager_certificatefor", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".certificatefor()" + }, + { + "label": ".loadFromDB()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L80", + "_origin": "ast", + "id": "tlsutil_acmemanager_loadfromdb", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".loadfromdb()" + }, + { + "label": ".obtainAndStore()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L108", + "_origin": "ast", + "id": "tlsutil_acmemanager_obtainandstore", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".obtainandstore()" + }, + { + "label": ".loadOrCreateAccountKey()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L160", + "_origin": "ast", + "id": "tlsutil_acmemanager_loadorcreateaccountkey", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".loadorcreateaccountkey()" + }, + { + "label": ".StartRenewalLoop()", + "file_type": "code", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L198", + "_origin": "ast", + "id": "tlsutil_acmemanager_startrenewalloop", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": ".startrenewalloop()" + }, + { + "label": "Context", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_go_context", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "context" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_acme_manager_go_duration", + "community": 1, + "community_name": "ACME/JWS Client", + "norm_label": "duration" + }, + { + "label": "selfsigned.go", + "file_type": "code", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_tlsutil_selfsigned", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "selfsigned.go" + }, + { + "label": "LoadOrGenerate()", + "file_type": "code", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L27", + "_origin": "ast", + "id": "internal_tlsutil_selfsigned_loadorgenerate", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "loadorgenerate()" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_selfsigned_go_config", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "config" + }, + { + "label": "ParseMinVersion()", + "file_type": "code", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L70", + "_origin": "ast", + "id": "internal_tlsutil_selfsigned_parseminversion", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "parseminversion()" + }, + { + "label": "generateSelfSigned()", + "file_type": "code", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L77", + "_origin": "ast", + "id": "internal_tlsutil_selfsigned_generateselfsigned", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "generateselfsigned()" + }, + { + "label": "Certificate", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_tlsutil_selfsigned_go_certificate", + "community": 5, + "community_name": "Server Config & Bootstrap", + "norm_label": "certificate" + }, + { + "label": "totp.go", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_totp_totp", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "totp.go" + }, + { + "label": "GenerateSecret()", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L29", + "_origin": "ast", + "id": "internal_totp_totp_generatesecret", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "generatesecret()" + }, + { + "label": "Generate()", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L40", + "_origin": "ast", + "id": "internal_totp_totp_generate", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "generate()" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_totp_totp_go_time", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "time" + }, + { + "label": "Validate()", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L52", + "_origin": "ast", + "id": "internal_totp_totp_validate", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "validate()" + }, + { + "label": "hotp()", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L71", + "_origin": "ast", + "id": "internal_totp_totp_hotp", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "hotp()" + }, + { + "label": "decodeSecret()", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L92", + "_origin": "ast", + "id": "internal_totp_totp_decodesecret", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "decodesecret()" + }, + { + "label": "ProvisioningURI()", + "file_type": "code", + "source_file": "internal/totp/totp.go", + "source_location": "L114", + "_origin": "ast", + "id": "internal_totp_totp_provisioninguri", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "provisioninguri()" + }, + { + "label": "vcard.go", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_vcard_vcard", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "vcard.go" + }, + { + "label": "Card", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L14", + "_origin": "ast", + "id": "vcard_card", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "card" + }, + { + "label": "Parse()", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L25", + "_origin": "ast", + "id": "internal_vcard_vcard_parse", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "parse()" + }, + { + "label": ".Build()", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L77", + "_origin": "ast", + "id": "vcard_card_build", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": ".build()" + }, + { + "label": "splitProperty()", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L106", + "_origin": "ast", + "id": "internal_vcard_vcard_splitproperty", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "splitproperty()" + }, + { + "label": "unfold()", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L121", + "_origin": "ast", + "id": "internal_vcard_vcard_unfold", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "unfold()" + }, + { + "label": "escape()", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L134", + "_origin": "ast", + "id": "internal_vcard_vcard_escape", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "escape()" + }, + { + "label": "unescape()", + "file_type": "code", + "source_file": "internal/vcard/vcard.go", + "source_location": "L142", + "_origin": "ast", + "id": "internal_vcard_vcard_unescape", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "unescape()" + }, + { + "label": "vcard_fuzz_test.go", + "file_type": "code", + "source_file": "internal/vcard/vcard_fuzz_test.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_vcard_vcard_fuzz_test", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "vcard_fuzz_test.go" + }, + { + "label": "FuzzParse()", + "file_type": "code", + "source_file": "internal/vcard/vcard_fuzz_test.go", + "source_location": "L5", + "_origin": "ast", + "id": "internal_vcard_vcard_fuzz_test_fuzzparse", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "fuzzparse()" + }, + { + "label": "F", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_vcard_vcard_fuzz_test_go_f", + "community": 26, + "community_name": "vCard Parsing & Fuzzing", + "norm_label": "f" + }, + { + "label": "webmail/api.go", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_webmail_api", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "webmail/api.go" + }, + { + "label": "Handler", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L36", + "_origin": "ast", + "id": "webmail_handler", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "handler" + }, + { + "label": "DB", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_db", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "db" + }, + { + "label": "Config", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_config", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "config" + }, + { + "label": "Mutex", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_mutex", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "mutex" + }, + { + "label": "oauthStateEntry", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L48", + "_origin": "ast", + "id": "webmail_oauthstateentry", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "oauthstateentry" + }, + { + "label": "Time", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_time", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "time" + }, + { + "label": "NewHandler()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "_origin": "ast", + "id": "internal_webmail_api_newhandler", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "newhandler()" + }, + { + "label": ".RegisterRoutes()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L62", + "_origin": "ast", + "id": "webmail_handler_registerroutes", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".registerroutes()" + }, + { + "label": "ServeMux", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_servemux", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "servemux" + }, + { + "label": "writeJSON()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L88", + "_origin": "ast", + "id": "internal_webmail_api_writejson", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "writejson()" + }, + { + "label": "ResponseWriter", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_responsewriter", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "responsewriter" + }, + { + "label": "writeErr()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L94", + "_origin": "ast", + "id": "internal_webmail_api_writeerr", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "writeerr()" + }, + { + "label": "ctxKey", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L100", + "_origin": "ast", + "id": "webmail_ctxkey", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "ctxkey" + }, + { + "label": ".login()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L104", + "_origin": "ast", + "id": "webmail_handler_login", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".login()" + }, + { + "label": "Request", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_request", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "request" + }, + { + "label": ".mfaVerify()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L151", + "_origin": "ast", + "id": "webmail_handler_mfaverify", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".mfaverify()" + }, + { + "label": ".withAuth()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L208", + "_origin": "ast", + "id": "webmail_handler_withauth", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".withauth()" + }, + { + "label": "HandlerFunc", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_handlerfunc", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "handlerfunc" + }, + { + "label": ".getMe()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L244", + "_origin": "ast", + "id": "webmail_handler_getme", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".getme()" + }, + { + "label": ".provider()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L252", + "_origin": "ast", + "id": "webmail_handler_provider", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".provider()" + }, + { + "label": ".listFolders()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L256", + "_origin": "ast", + "id": "webmail_handler_listfolders", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".listfolders()" + }, + { + "label": ".listMessages()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L266", + "_origin": "ast", + "id": "webmail_handler_listmessages", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".listmessages()" + }, + { + "label": ".sendOrListMessages()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L291", + "_origin": "ast", + "id": "webmail_handler_sendorlistmessages", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".sendorlistmessages()" + }, + { + "label": ".messageByID()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L320", + "_origin": "ast", + "id": "webmail_handler_messagebyid", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".messagebyid()" + }, + { + "label": ".listQuarantine()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L381", + "_origin": "ast", + "id": "webmail_handler_listquarantine", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".listquarantine()" + }, + { + "label": ".releaseQuarantine()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L390", + "_origin": "ast", + "id": "webmail_handler_releasequarantine", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".releasequarantine()" + }, + { + "label": ".sseEvents()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L437", + "_origin": "ast", + "id": "webmail_handler_sseevents", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".sseevents()" + }, + { + "label": ".listAccounts()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L472", + "_origin": "ast", + "id": "webmail_handler_listaccounts", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".listaccounts()" + }, + { + "label": ".deleteAccount()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L498", + "_origin": "ast", + "id": "webmail_handler_deleteaccount", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".deleteaccount()" + }, + { + "label": ".oauthDispatch()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L524", + "_origin": "ast", + "id": "webmail_handler_oauthdispatch", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".oauthdispatch()" + }, + { + "label": ".oauthStart()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L545", + "_origin": "ast", + "id": "webmail_handler_oauthstart", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".oauthstart()" + }, + { + "label": ".oauthCallback()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L566", + "_origin": "ast", + "id": "webmail_handler_oauthcallback", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".oauthcallback()" + }, + { + "label": ".pruneExpiredState()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L632", + "_origin": "ast", + "id": "webmail_handler_pruneexpiredstate", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".pruneexpiredstate()" + }, + { + "label": "randomState()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L641", + "_origin": "ast", + "id": "internal_webmail_api_randomstate", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "randomstate()" + }, + { + "label": "sha256Hex()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L651", + "_origin": "ast", + "id": "internal_webmail_api_sha256hex", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "sha256hex()" + }, + { + "label": ".mfaSetup()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L659", + "_origin": "ast", + "id": "webmail_handler_mfasetup", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".mfasetup()" + }, + { + "label": ".mfaConfirm()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L685", + "_origin": "ast", + "id": "webmail_handler_mfaconfirm", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".mfaconfirm()" + }, + { + "label": ".mfaDisable()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L733", + "_origin": "ast", + "id": "webmail_handler_mfadisable", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".mfadisable()" + }, + { + "label": ".appPasswords()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L759", + "_origin": "ast", + "id": "webmail_handler_apppasswords", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".apppasswords()" + }, + { + "label": ".appPasswordByID()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L832", + "_origin": "ast", + "id": "webmail_handler_apppasswordbyid", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".apppasswordbyid()" + }, + { + "label": "parseDuration()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L850", + "_origin": "ast", + "id": "internal_webmail_api_parseduration", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "parseduration()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webmail_api_go_duration", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "duration" + }, + { + "label": ".forgotPassword()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L867", + "_origin": "ast", + "id": "webmail_handler_forgotpassword", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".forgotpassword()" + }, + { + "label": ".resetPassword()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L894", + "_origin": "ast", + "id": "webmail_handler_resetpassword", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".resetpassword()" + }, + { + "label": ".setRecoveryEmail()", + "file_type": "code", + "source_file": "internal/webmail/api.go", + "source_location": "L921", + "_origin": "ast", + "id": "webmail_handler_setrecoveryemail", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": ".setrecoveryemail()" + }, + { + "label": "webmail/embed.go", + "file_type": "code", + "source_file": "internal/webmail/embed.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_webmail_embed", + "community": 60, + "community_name": "Embedded Assets (webmail)", + "norm_label": "webmail/embed.go" + }, + { + "label": "webtoken.go", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L1", + "_origin": "ast", + "id": "internal_webtoken_webtoken", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "webtoken.go" + }, + { + "label": "Claims", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L21", + "_origin": "ast", + "id": "webtoken_claims", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "claims" + }, + { + "label": "Issue()", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L33", + "_origin": "ast", + "id": "internal_webtoken_webtoken_issue", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "issue()" + }, + { + "label": "Duration", + "file_type": "code", + "source_file": "", + "source_location": "", + "_origin": "ast", + "id": "internal_webtoken_webtoken_go_duration", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "duration" + }, + { + "label": "IssueWithPurpose()", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L41", + "_origin": "ast", + "id": "internal_webtoken_webtoken_issuewithpurpose", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "issuewithpurpose()" + }, + { + "label": "Verify()", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L59", + "_origin": "ast", + "id": "internal_webtoken_webtoken_verify", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "verify()" + }, + { + "label": "sign()", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L87", + "_origin": "ast", + "id": "internal_webtoken_webtoken_sign", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "sign()" + }, + { + "label": "base64URLEncode()", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L93", + "_origin": "ast", + "id": "internal_webtoken_webtoken_base64urlencode", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "base64urlencode()" + }, + { + "label": "base64URLDecode()", + "file_type": "code", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L97", + "_origin": "ast", + "id": "internal_webtoken_webtoken_base64urldecode", + "community": 0, + "community_name": "TOTP & Web Token Auth", + "norm_label": "base64urldecode()" + }, + { + "label": "Go Web App No-Deps Pattern (skill)", + "file_type": "concept", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_pattern", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "go web app no-deps pattern (skill)" + }, + { + "label": "Template Renderer (fresh-instance-per-page)", + "file_type": "rationale", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_renderer", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "template renderer (fresh-instance-per-page)" + }, + { + "label": "Template Block Bleeding Bug (ParseGlob shared namespace)", + "file_type": "rationale", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_block_bleed_bug", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "template block bleeding bug (parseglob shared namespace)" + }, + { + "label": "base.html Flask-style Block Layout", + "file_type": "concept", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_base_html", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "base.html flask-style block layout" + }, + { + "label": "Static Asset Cache-Busting via Version Query Param", + "file_type": "rationale", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_cache_busting", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "static asset cache-busting via version query param" + }, + { + "label": "Dark Tailwind CSS Custom-Property Palette", + "file_type": "concept", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_dark_theme", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "dark tailwind css custom-property palette" + }, + { + "label": "JS Function-in-Conditional Scoping Bug", + "file_type": "rationale", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_js_scoping_bug", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "js function-in-conditional scoping bug" + }, + { + "label": "Shared api() Fetch Helper Convention", + "file_type": "concept", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_api_helper", + "community": 47, + "community_name": "Shared api() Fetch Convention", + "norm_label": "shared api() fetch helper convention" + }, + { + "label": "Security Basics: CSRF/Path-Traversal/Atomic-Write/CSP", + "file_type": "rationale", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_security_basics", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "security basics: csrf/path-traversal/atomic-write/csp" + }, + { + "label": "No-Third-Party-Dependencies Principle", + "file_type": "rationale", + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_skill_no_deps_principle", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "no-third-party-dependencies principle" + }, + { + "label": "go-web-app-no-deps Packaged Skill (.skill zip)", + "file_type": "document", + "source_file": ".claude/go-web-app-no-deps.skill", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_go_web_app_no_deps_package", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "go-web-app-no-deps packaged skill (.skill zip)" + }, + { + "label": "Core Principle: Compiles is Not Correct", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_core_principle", + "community": 37, + "community_name": "Iterative Build Discipline Skill", + "norm_label": "core principle: compiles is not correct" + }, + { + "label": "Disposable Real End-to-End Test Pattern", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_e2e_testing", + "community": 37, + "community_name": "Iterative Build Discipline Skill", + "norm_label": "disposable real end-to-end test pattern" + }, + { + "label": "Genuinely-Enforcing Fake Protocol Server Pattern", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_fake_protocol_server", + "community": 37, + "community_name": "Iterative Build Discipline Skill", + "norm_label": "genuinely-enforcing fake protocol server pattern" + }, + { + "label": "Verify Against Published Test Vectors (RFC 6238)", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_test_vectors", + "community": 37, + "community_name": "Iterative Build Discipline Skill", + "norm_label": "verify against published test vectors (rfc 6238)" + }, + { + "label": "Negative-Path Tests as Non-Optional", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_negative_tests", + "community": 37, + "community_name": "Iterative Build Discipline Skill", + "norm_label": "negative-path tests as non-optional" + }, + { + "label": "Checkpoint Working State as Durable Artifact", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_checkpoint_artifact", + "community": 48, + "community_name": "Session Continuity Practices", + "norm_label": "checkpoint working state as durable artifact" + }, + { + "label": "Living Plan Doc + Separate Handover Doc", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_living_docs", + "community": 48, + "community_name": "Session Continuity Practices", + "norm_label": "living plan doc + separate handover doc" + }, + { + "label": "Explicit Scope Deferral Over Half-Building", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_scope_honesty", + "community": 49, + "community_name": "Scope Prioritization Practices", + "norm_label": "explicit scope deferral over half-building" + }, + { + "label": "Prioritize by Risk Reduction Over Task Order", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_risk_prioritization", + "community": 49, + "community_name": "Scope Prioritization Practices", + "norm_label": "prioritize by risk reduction over task order" + }, + { + "label": "bufio.Reader + io.ReadFull for TCP Protocol Boundaries", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_tcp_read_pattern", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "bufio.reader + io.readfull for tcp protocol boundaries" + }, + { + "label": "SQLite Single-Connection-Pool Query/Exec Deadlock Pattern", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_sqlite_single_conn", + "community": 41, + "community_name": "SQLite Deadlock Findings", + "norm_label": "sqlite single-connection-pool query/exec deadlock pattern" + }, + { + "label": "Wire-Format Structs Need Explicit Serialization Tags", + "file_type": "rationale", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_wire_struct_tags", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "wire-format structs need explicit serialization tags" + }, + { + "label": "GoMail (referenced project)", + "file_type": "concept", + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_skill_gomail_project", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "gomail (referenced project)" + }, + { + "label": "iterative-build-discipline Packaged Skill (.skill zip)", + "file_type": "document", + "source_file": ".claude/iterative-build-discipline.skill", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "_claude_iterative_build_discipline_package", + "community": 37, + "community_name": "Iterative Build Discipline Skill", + "norm_label": "iterative-build-discipline packaged skill (.skill zip)" + }, + { + "label": "graphify Project Rules", + "file_type": "document", + "source_file": "CLAUDE.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "claude_graphify_rules", + "community": 54, + "community_name": "Graphify Project Rules", + "norm_label": "graphify project rules" + }, + { + "label": "GoMail Project Overview (Handover)", + "file_type": "document", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_overview", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "gomail project overview (handover)" + }, + { + "label": "Immediate First Steps Setup", + "file_type": "document", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_setup_steps", + "community": 38, + "community_name": "Deployment & DNS Setup", + "norm_label": "immediate first steps setup" + }, + { + "label": "Don't Add Third-Party Protocol Libraries Rule", + "file_type": "rationale", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_no_third_party_rule", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "don't add third-party protocol libraries rule" + }, + { + "label": "Design Decisions Not To Re-litigate", + "file_type": "rationale", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_decisions_not_to_relitigate", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "design decisions not to re-litigate" + }, + { + "label": "Suggested Next Session Scope", + "file_type": "document", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_next_session_scope", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "suggested next session scope" + }, + { + "label": "Fake-Server TCP Read Over-Read Bug (documented)", + "file_type": "rationale", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_tcp_bug", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "fake-server tcp read over-read bug (documented)" + }, + { + "label": "SQLite Query+Exec Deadlock Bug (documented)", + "file_type": "rationale", + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_handover_sqlite_deadlock_bug", + "community": 41, + "community_name": "SQLite Deadlock Findings", + "norm_label": "sqlite query+exec deadlock bug (documented)" + }, + { + "label": "GoMail (Self-Hosted Email Server)", + "file_type": "document", + "source_file": "README.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "readme_gomail", + "community": 38, + "community_name": "Deployment & DNS Setup", + "norm_label": "gomail (self-hosted email server)" + }, + { + "label": "TLS / ACME Quick Setup", + "file_type": "document", + "source_file": "README.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "readme_tls_acme", + "community": 38, + "community_name": "Deployment & DNS Setup", + "norm_label": "tls / acme quick setup" + }, + { + "label": "DNS Setup (MX/SPF/DKIM/DMARC)", + "file_type": "document", + "source_file": "README.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "readme_dns_setup", + "community": 38, + "community_name": "Deployment & DNS Setup", + "norm_label": "dns setup (mx/spf/dkim/dmarc)" + }, + { + "label": "GoMail Action Plan v4 Overview", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_overview", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "gomail action plan v4 overview" + }, + { + "label": "Phase 5: IMAP/POP3/Auth", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase5", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "phase 5: imap/pop3/auth" + }, + { + "label": "Phase 7: CalDAV/CardDAV", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase7", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "phase 7: caldav/carddav" + }, + { + "label": "Phase 8: Webmail (JWT + REST API + SPA)", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase8", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "phase 8: webmail (jwt + rest api + spa)" + }, + { + "label": "Phase 9: JMAP Core/Mail Subset", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase9", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "phase 9: jmap core/mail subset" + }, + { + "label": "Phase 9.5: ManageSieve + Sieve Interpreter", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase9_5", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "phase 9.5: managesieve + sieve interpreter" + }, + { + "label": "Phase 10: OAuth2 + Gmail/M365 Multi-Account", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase10", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "phase 10: oauth2 + gmail/m365 multi-account" + }, + { + "label": "Phase 11: Admin Portal", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase11", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "phase 11: admin portal" + }, + { + "label": "Phase 12: Auth Hardening (TOTP/App Passwords/Reset)", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase12", + "community": 41, + "community_name": "SQLite Deadlock Findings", + "norm_label": "phase 12: auth hardening (totp/app passwords/reset)" + }, + { + "label": "Phase 13: TLS + ACME + DANE/MTA-STS", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase13", + "community": 38, + "community_name": "Deployment & DNS Setup", + "norm_label": "phase 13: tls + acme + dane/mta-sts" + }, + { + "label": "Phase 14: Optional External Services (ClamAV/Rspamd/LLM)", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase14", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "phase 14: optional external services (clamav/rspamd/llm)" + }, + { + "label": "Phase 15: Hardening + Deploy", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase15", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "phase 15: hardening + deploy" + }, + { + "label": "Rationale: Phase 13 Pulled Forward", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_phase13_pulled_forward_rationale", + "community": 41, + "community_name": "SQLite Deadlock Findings", + "norm_label": "rationale: phase 13 pulled forward" + }, + { + "label": "Genuine EICAR Byte-Level ClamAV Verification", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_eicar_verification", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "genuine eicar byte-level clamav verification" + }, + { + "label": "Token-Bucket Per-IP Rate Limiter", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_rate_limiter", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "token-bucket per-ip rate limiter" + }, + { + "label": "ConsumeBackupCode SQLite Deadlock Bug", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_sqlite_deadlock_bug", + "community": 41, + "community_name": "SQLite Deadlock Findings", + "norm_label": "consumebackupcode sqlite deadlock bug" + }, + { + "label": "Fake clamd Server TCP Over-Read Bug", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_tcp_bug", + "community": 30, + "community_name": "TCP Boundary & Rate-Limit Findings", + "norm_label": "fake clamd server tcp over-read bug" + }, + { + "label": "Quarantine: Admin Global Discard vs Webmail Per-User Release", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_quarantine_admin_vs_webmail", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "quarantine: admin global discard vs webmail per-user release" + }, + { + "label": "Sandbox Network/Toolchain Constraints", + "file_type": "document", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_sandbox_constraints", + "community": 38, + "community_name": "Deployment & DNS Setup", + "norm_label": "sandbox network/toolchain constraints" + }, + { + "label": "cmd/e2etestN Disposable Test Pattern", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_e2etest_pattern", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "cmd/e2etestn disposable test pattern" + }, + { + "label": "Admin Domain Creation Missing Tenant Fallback Bug", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_admin_domain_tenant_bug", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "admin domain creation missing tenant fallback bug" + }, + { + "label": "Admin User Creation Missing domain_id Bug", + "file_type": "rationale", + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "gomail_action_plan_v4_admin_user_domainid_bug", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "admin user creation missing domain_id bug" + }, + { + "label": "GoMail Admin SPA (index.html)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_page", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "gomail admin spa (index.html)" + }, + { + "label": "api() fetch helper (admin)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_api", + "community": 47, + "community_name": "Shared api() Fetch Convention", + "norm_label": "api() fetch helper (admin)" + }, + { + "label": "login() (admin)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_login", + "community": 52, + "community_name": "SPA Login/Logout", + "norm_label": "login() (admin)" + }, + { + "label": "showApp()/showLogin() (admin)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_showapp", + "community": 53, + "community_name": "SPA App Boot/Routing", + "norm_label": "showapp()/showlogin() (admin)" + }, + { + "label": "showPage() router (admin)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_showpage", + "community": 58, + "community_name": "Admin SPA Router", + "norm_label": "showpage() router (admin)" + }, + { + "label": "loadDashboard()", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_loaddashboard", + "community": 57, + "community_name": "Admin Dashboard Loader", + "norm_label": "loaddashboard()" + }, + { + "label": "Domains CRUD (loadDomains/createDomain/rotateDkim/deleteDomain)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_domains_crud", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "domains crud (loaddomains/createdomain/rotatedkim/deletedomain)" + }, + { + "label": "Users CRUD (loadUsers/createUser/suspendUser/activateUser/deleteUser)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_users_crud", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "users crud (loadusers/createuser/suspenduser/activateuser/deleteuser)" + }, + { + "label": "List Rules CRUD", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_rules_crud", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "list rules crud" + }, + { + "label": "Outbound Queue Actions (retry/cancel)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_queue_crud", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "outbound queue actions (retry/cancel)" + }, + { + "label": "Quarantine Discard (admin)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_quarantine", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "quarantine discard (admin)" + }, + { + "label": "esc() HTML-escape helper (admin)", + "file_type": "code", + "source_file": "internal/admin/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_admin_static_index_esc", + "community": 51, + "community_name": "Shared esc() Helper", + "norm_label": "esc() html-escape helper (admin)" + }, + { + "label": "GoMail Webmail SPA (index.html)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_page", + "community": 21, + "community_name": "Go-No-Deps Web App Pattern", + "norm_label": "gomail webmail spa (index.html)" + }, + { + "label": "api() fetch helper (webmail)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_api", + "community": 47, + "community_name": "Shared api() Fetch Convention", + "norm_label": "api() fetch helper (webmail)" + }, + { + "label": "login()/logout() (webmail)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_login", + "community": 52, + "community_name": "SPA Login/Logout", + "norm_label": "login()/logout() (webmail)" + }, + { + "label": "showApp()/boot() (webmail)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_showapp", + "community": 53, + "community_name": "SPA App Boot/Routing", + "norm_label": "showapp()/boot() (webmail)" + }, + { + "label": "loadFolders()/selectFolder()", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_folders", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "loadfolders()/selectfolder()" + }, + { + "label": "loadMessages()/viewMessage()/deleteMessage()", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_messages", + "community": 62, + "community_name": "Webmail Message Actions", + "norm_label": "loadmessages()/viewmessage()/deletemessage()" + }, + { + "label": "Compose (openCompose/closeCompose/sendMessage)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_compose", + "community": 22, + "community_name": "GoMail Build Phases Overview", + "norm_label": "compose (opencompose/closecompose/sendmessage)" + }, + { + "label": "Quarantine Release (showQuarantine/releaseQ)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_quarantine", + "community": 27, + "community_name": "Admin Portal Bugs & CRUD", + "norm_label": "quarantine release (showquarantine/releaseq)" + }, + { + "label": "esc() HTML-escape helper (webmail)", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_esc", + "community": 51, + "community_name": "Shared esc() Helper", + "norm_label": "esc() html-escape helper (webmail)" + }, + { + "label": "bodyOf() raw MIME body extractor", + "file_type": "code", + "source_file": "internal/webmail/static/index.html", + "source_location": null, + "source_url": null, + "captured_at": null, + "author": null, + "contributor": null, + "id": "internal_webmail_static_index_bodyof", + "community": 61, + "community_name": "Raw MIME Body Extractor", + "norm_label": "bodyof() raw mime body extractor" + } + ], + "links": [ + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "cmd/gomail/main.go", + "source_location": "L332", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main", + "target": "cmd_gomail_main_buildoauthconfigs", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "cmd/gomail/main.go", + "source_location": "L38", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main", + "target": "cmd_gomail_main_main", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "cmd/gomail/main.go", + "source_location": "L239", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "cmd_gomail_main_buildoauthconfigs", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L112", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_acme_challenge_newchallengeresponder" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L70", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_config_config_load" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L160", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_crypto_crypto_generatemasterkeyhex" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_crypto_crypto_loadmasterkey" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L85", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_db_db_open" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_pipeline_pipeline_neworchestrator" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_pipeline_pipeline_stagesfromconfig" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L152", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_queue_queue_newworker" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_tlsutil_acme_manager_newacmemanager" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L127", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_tlsutil_selfsigned_loadorgenerate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L129", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_main", + "target": "internal_tlsutil_selfsigned_parseminversion" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "cmd/gomail/main.go", + "source_location": "L332", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "cmd_gomail_main_buildoauthconfigs", + "target": "cmd_gomail_main_go_config", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "cmd/gomail/main.go", + "source_location": "L336", + "weight": 1.0, + "_origin": "ast", + "source": "cmd_gomail_main_buildoauthconfigs", + "target": "internal_oauth2_oauth2_wellknownendpoints" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link", + "target": "internal_accounts_link_linkimapaccount", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link", + "target": "internal_accounts_link_linkoauth2account", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link", + "target": "internal_accounts_link_providerfor", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link", + "target": "internal_accounts_link_wellknownimaphost", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L18", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_linkimapaccount", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L18", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_link_linkimapaccount", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L18", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_linkimapaccount", + "target": "internal_accounts_link_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/link.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link_linkimapaccount", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "internal_accounts_link_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "internal_accounts_link_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L77", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "internal_accounts_link_wellknownimaphost", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L58", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_wellknownimaphost", + "target": "db_linkedaccountprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "db_linkedaccountprovider", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/link.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_linkoauth2account", + "target": "oauth2_token", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L622", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthcallback", + "target": "internal_accounts_link_linkoauth2account" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "accounts_mailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/link.go", + "source_location": "L128", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "internal_accounts_link_go_config", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/link.go", + "source_location": "L131", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "internal_accounts_provider_imap_newimapprovider" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/link.go", + "source_location": "L138", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_link_providerfor", + "target": "internal_accounts_provider_imap_newimapprovideroauth2" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L10", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_folder", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L29", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_fullmessage", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_listopts", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_mailprovider", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_messageheader", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_outgoingmessage", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider", + "target": "accounts_syncresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L32", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_gomailprovider_listfolders", + "target": "accounts_folder", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L152", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_imapprovider_listfolders", + "target": "accounts_folder", + "confidence_score": 1.0 + }, + { + "relation": "embeds", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_fullmessage", + "target": "accounts_messageheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L60", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_gomailprovider_listmessages", + "target": "accounts_messageheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L178", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_imapprovider_listmessages", + "target": "accounts_messageheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider.go", + "source_location": "L50", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_syncresult", + "target": "accounts_messageheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L220", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_provider_gomail_headerfromraw", + "target": "accounts_messageheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L92", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_gomailprovider_getmessage", + "target": "accounts_fullmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L230", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_imapprovider_getmessage", + "target": "accounts_fullmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L115", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_sendmessage", + "target": "accounts_outgoingmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L264", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_sendmessage", + "target": "accounts_outgoingmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L243", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_gomail_buildrfc5322", + "target": "accounts_outgoingmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L18", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper_sendviasmtp", + "target": "accounts_outgoingmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L60", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_listmessages", + "target": "accounts_listopts", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L178", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_listmessages", + "target": "accounts_listopts", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L197", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_gomailprovider_sync", + "target": "accounts_syncresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L306", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_imapprovider_sync", + "target": "accounts_syncresult", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L22", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_gomail", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L243", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_gomail", + "target": "internal_accounts_provider_gomail_buildrfc5322", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L235", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_gomail", + "target": "internal_accounts_provider_gomail_domainof", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L203", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_gomail", + "target": "internal_accounts_provider_gomail_foldertype", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L220", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_gomail", + "target": "internal_accounts_provider_gomail_headerfromraw", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L28", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_gomail", + "target": "internal_accounts_provider_gomail_newgomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L179", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_delete", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_getmessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L32", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_listfolders", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L60", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_listmessages", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L168", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_move", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L115", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_sendmessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L149", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_setflags", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L197", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "accounts_gomailprovider_sync", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L25", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "internal_accounts_provider_gomail_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L24", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_gomailprovider", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L28", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_provider_gomail_newgomailprovider", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L156", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L248", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_emailget", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L221", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_emailquery", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L177", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_mailboxget", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L252", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "webmail_handler_provider", + "target": "accounts_gomailprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L28", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_gomail_newgomailprovider", + "target": "internal_accounts_provider_gomail_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L28", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_gomail_newgomailprovider", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L28", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_gomail_newgomailprovider", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/jmap/jmap.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_api", + "target": "internal_accounts_provider_gomail_newgomailprovider" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L253", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_provider", + "target": "internal_accounts_provider_gomail_newgomailprovider" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_listfolders", + "target": "internal_accounts_provider_gomail_foldertype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L32", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_listfolders", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L179", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_delete", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L92", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_getmessage", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L60", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_listmessages", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L168", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_move", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L115", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_sendmessage", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L149", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_setflags", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L197", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_gomailprovider_sync", + "target": "internal_accounts_provider_gomail_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L87", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_listmessages", + "target": "internal_accounts_provider_gomail_headerfromraw", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L104", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_getmessage", + "target": "internal_accounts_provider_gomail_headerfromraw", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L169", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_move", + "target": "accounts_gomailprovider_getmessage", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L116", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_sendmessage", + "target": "internal_accounts_provider_gomail_buildrfc5322", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L119", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_sendmessage", + "target": "internal_accounts_provider_gomail_domainof", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_gomail.go", + "source_location": "L176", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_gomailprovider_move", + "target": "accounts_gomailprovider_delete", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L172", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_listfolders", + "target": "internal_accounts_provider_gomail_foldertype" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper_sendviasmtp", + "target": "internal_accounts_provider_gomail_buildrfc5322" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L23", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_imap", + "target": "accounts_imapcredential", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L42", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_imap", + "target": "accounts_imapprovider", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_imap", + "target": "accounts_oauth2credential", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L49", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_imap", + "target": "internal_accounts_provider_imap_newimapprovider", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_imap", + "target": "internal_accounts_provider_imap_newimapprovideroauth2", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L34", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_oauth2credential", + "target": "internal_accounts_provider_imap_go_time", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L63", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_connect", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L290", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_delete", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L230", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_getmessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L152", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_listfolders", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L178", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_listmessages", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L112", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_loginoauth2", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L281", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_move", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L264", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_sendmessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L268", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_setflags", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L306", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "accounts_imapprovider_sync", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L44", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L43", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L46", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "internal_accounts_provider_imap_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L45", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "accounts_imapprovider", + "target": "internal_accounts_provider_imap_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L49", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovider", + "target": "accounts_imapprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovideroauth2", + "target": "accounts_imapprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovideroauth2", + "target": "internal_accounts_provider_imap_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovideroauth2", + "target": "internal_accounts_provider_imap_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L49", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovider", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L49", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovider", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovideroauth2", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_imap_newimapprovideroauth2", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_connect", + "target": "accounts_imapprovider_loginoauth2", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L63", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "accounts_imapprovider_connect", + "target": "client", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L63", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_connect", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L95", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_connect", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_connect", + "target": "internal_imapclient_client_dial" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L291", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_delete", + "target": "accounts_imapprovider_connect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L231", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_getmessage", + "target": "accounts_imapprovider_connect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L153", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_listfolders", + "target": "accounts_imapprovider_connect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L179", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_listmessages", + "target": "accounts_imapprovider_connect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L269", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_setflags", + "target": "accounts_imapprovider_connect", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L290", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_delete", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L230", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_getmessage", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L152", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_listfolders", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L178", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_listmessages", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L112", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_loginoauth2", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L281", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_move", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L264", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_sendmessage", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L268", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_setflags", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L306", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_sync", + "target": "internal_accounts_provider_imap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L112", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "accounts_imapprovider_loginoauth2", + "target": "client", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_loginoauth2", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_loginoauth2", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L145", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_loginoauth2", + "target": "internal_oauth2_oauth2_xoauth2saslstring" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L309", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_sync", + "target": "accounts_imapprovider_listfolders", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L315", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_sync", + "target": "accounts_imapprovider_listmessages", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_imap.go", + "source_location": "L265", + "weight": 1.0, + "_origin": "ast", + "source": "accounts_imapprovider_sendmessage", + "target": "internal_accounts_provider_smtp_helper_sendviasmtp" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper", + "target": "internal_accounts_provider_smtp_helper_sendviasmtp", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L18", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper_sendviasmtp", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L18", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper_sendviasmtp", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper_sendviasmtp", + "target": "internal_acme_client_newclient" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/accounts/provider_smtp_helper.go", + "source_location": "L19", + "weight": 1.0, + "_origin": "ast", + "source": "internal_accounts_provider_smtp_helper_sendviasmtp", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L13", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_challenge", + "target": "acme_challengeresponder", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_challenge", + "target": "internal_acme_challenge_newchallengeresponder", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L28", + "weight": 1.0, + "_origin": "ast", + "source": "acme_challengeresponder", + "target": "acme_challengeresponder_remove", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "acme_challengeresponder", + "target": "acme_challengeresponder_servehttp", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L22", + "weight": 1.0, + "_origin": "ast", + "source": "acme_challengeresponder", + "target": "acme_challengeresponder_set", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L14", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "acme_challengeresponder", + "target": "internal_acme_challenge_go_rwmutex", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L18", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_acme_challenge_newchallengeresponder", + "target": "acme_challengeresponder", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/obtain.go", + "source_location": "L14", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_acme_obtain_obtain", + "target": "acme_challengeresponder", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L37", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_tlsutil_acme_manager_newacmemanager", + "target": "acme_challengeresponder", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L31", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "acme_challengeresponder", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L189", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_deletequeuefile", + "target": "acme_challengeresponder_remove" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L60", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_deliver", + "target": "acme_challengeresponder_remove" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L216", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_writequarantinefile", + "target": "acme_challengeresponder_remove" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L179", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_writequeuefile", + "target": "acme_challengeresponder_remove" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L34", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "acme_challengeresponder_servehttp", + "target": "internal_acme_challenge_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/challenge.go", + "source_location": "L34", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "acme_challengeresponder_servehttp", + "target": "internal_acme_challenge_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L157", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "acme_authorization", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L165", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "acme_challenge", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L24", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "acme_client", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "acme_directory", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "acme_order", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L295", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "internal_acme_client_buildcsr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_client", + "target": "internal_acme_client_newclient", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L27", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "acme_client", + "target": "acme_directory", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L28", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "acme_client", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_bootstrap", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L233", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_finalizeanddownload", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L173", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_getauthorization", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L191", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_keyauthorization", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L99", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_newaccount", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L131", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_neworder", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L67", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_post", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L198", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_respondtochallenge", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L211", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client", + "target": "acme_client_waitforauthorizationvalid", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L33", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_acme_client_newclient", + "target": "acme_client", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L33", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_acme_client_newclient", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/acme/obtain.go", + "source_location": "L15", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_obtain_obtain", + "target": "internal_acme_client_newclient" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/queue/queue.go", + "source_location": "L282", + "weight": 1.0, + "_origin": "ast", + "source": "queue_mxdeliverer_delivertohost", + "target": "internal_acme_client_newclient" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L248", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_finalizeanddownload", + "target": "acme_client_post", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L174", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_getauthorization", + "target": "acme_client_post", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L108", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_newaccount", + "target": "acme_client_post", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L141", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_neworder", + "target": "acme_client_post", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L67", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "acme_client_post", + "target": "response", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L199", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_respondtochallenge", + "target": "acme_client_post", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L233", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "acme_client_finalizeanddownload", + "target": "acme_order", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L131", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "acme_client_neworder", + "target": "acme_order", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L162", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "acme_authorization", + "target": "acme_challenge", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L173", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "acme_client_getauthorization", + "target": "acme_authorization", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L214", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_waitforauthorizationvalid", + "target": "acme_client_getauthorization", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L211", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "acme_client_waitforauthorizationvalid", + "target": "internal_acme_client_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L233", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "acme_client_finalizeanddownload", + "target": "internal_acme_client_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L239", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_finalizeanddownload", + "target": "internal_acme_client_buildcsr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/acme/client.go", + "source_location": "L244", + "weight": 1.0, + "_origin": "ast", + "source": "acme_client_finalizeanddownload", + "target": "internal_acme_jws_b64" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/client.go", + "source_location": "L295", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_acme_client_buildcsr", + "target": "internal_acme_client_go_privatekey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L25", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_jws", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L60", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_jws", + "target": "acme_jwk", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L132", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_jws", + "target": "internal_acme_jws_b64", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L29", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_jws", + "target": "internal_acme_jws_generateaccountkey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L136", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_jws", + "target": "internal_acme_jws_leftpad", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L45", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_jws", + "target": "internal_acme_jws_parseaccountkeypem", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L67", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey", + "target": "acme_accountkey_jwkvalue", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L37", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey", + "target": "acme_accountkey_marshalpem", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L90", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey", + "target": "acme_accountkey_signjws", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey", + "target": "acme_accountkey_thumbprint", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L26", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "acme_accountkey", + "target": "internal_acme_jws_go_privatekey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L29", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_acme_jws_generateaccountkey", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L45", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_acme_jws_parseaccountkeypem", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/obtain.go", + "source_location": "L14", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_acme_obtain_obtain", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L160", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_loadorcreateaccountkey", + "target": "acme_accountkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L171", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_loadorcreateaccountkey", + "target": "internal_acme_jws_generateaccountkey" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L165", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_loadorcreateaccountkey", + "target": "internal_acme_jws_parseaccountkeypem" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L67", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "acme_accountkey_jwkvalue", + "target": "acme_jwk", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_jwkvalue", + "target": "internal_acme_jws_b64", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_jwkvalue", + "target": "internal_acme_jws_leftpad", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L97", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_signjws", + "target": "acme_accountkey_jwkvalue", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L79", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_thumbprint", + "target": "acme_accountkey_jwkvalue", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L83", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_thumbprint", + "target": "internal_acme_jws_b64", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_signjws", + "target": "internal_acme_jws_b64", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/acme/jws.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_signjws", + "target": "internal_acme_jws_leftpad", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/acme/jws.go", + "source_location": "L118", + "weight": 1.0, + "_origin": "ast", + "source": "acme_accountkey_signjws", + "target": "internal_dkim_sign_sign" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/acme/obtain.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "internal_acme_obtain", + "target": "internal_acme_obtain_obtain", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L115", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_obtainandstore", + "target": "internal_acme_obtain_obtain" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L300", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api", + "target": "internal_admin_api_filterdomainsbytenant", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L25", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api", + "target": "internal_admin_api_go_admin_handler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api", + "target": "internal_admin_api_newhandler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L293", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api", + "target": "internal_admin_api_scopetenant", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L239", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_domainbyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L185", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_domains", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L136", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_encryptdkimkey", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L65", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_login", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L35", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_registerroutes", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L142", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_stats", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L153", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_tenants", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L99", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "admin_handler_withadmin", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L27", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L26", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_admin_api_go_admin_handler", + "target": "internal_admin_api_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L31", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_admin_api_newhandler", + "target": "internal_admin_api_go_admin_handler", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L31", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_admin_api_newhandler", + "target": "internal_admin_api_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L31", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_admin_api_newhandler", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L37", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_registerroutes", + "target": "admin_handler_withadmin", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L35", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_registerroutes", + "target": "internal_admin_api_go_servemux", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L273", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L193", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L199", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_listrulebyid", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L154", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_listrules", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L93", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_login", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L257", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_quarantine", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L270", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_quarantinebyid", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L214", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_queue", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L231", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_queuebyid", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L148", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_stats", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L165", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_tenants", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L95", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_userbyid", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L23", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_users", + "target": "internal_admin_api_writejson" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L60", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_api_writeerr", + "target": "internal_admin_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L53", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_admin_api_writejson", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L239", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L185", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L65", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_login", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L142", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_stats", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L153", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_tenants", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L99", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_withadmin", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_admin_api_writeerr", + "target": "internal_admin_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L249", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L190", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L196", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_listrulebyid", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L143", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_listrules", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_login", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L254", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_quarantine", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L267", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_quarantinebyid", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L211", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_queue", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L228", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_queuebyid", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L145", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_stats", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L155", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_tenants", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L77", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_userbyid", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L20", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_users", + "target": "internal_admin_api_writeerr" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_withadmin", + "target": "internal_admin_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L65", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_login", + "target": "internal_admin_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/api.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_login", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/api.go", + "source_location": "L88", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_login", + "target": "internal_webtoken_webtoken_issue" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L239", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "internal_admin_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L185", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_admin_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L142", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_stats", + "target": "internal_admin_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L153", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_tenants", + "target": "internal_admin_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L99", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_withadmin", + "target": "internal_admin_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L99", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_withadmin", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L99", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "admin_handler_withadmin", + "target": "internal_admin_api_go_handlerfunc", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L264", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "admin_handler_encryptdkimkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L219", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domains", + "target": "admin_handler_encryptdkimkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/api.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_encryptdkimkey", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L142", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_stats", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L153", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_tenants", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L185", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_domains", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L193", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_admin_api_filterdomainsbytenant", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L193", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_admin_api_scopetenant", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/api.go", + "source_location": "L214", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domains", + "target": "internal_dkim_keys_generatekeypair" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L239", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/api.go", + "source_location": "L259", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_domainbyid", + "target": "internal_dkim_keys_generatekeypair" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_users", + "target": "internal_admin_api_scopetenant" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L293", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_admin_api_scopetenant", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/api.go", + "source_location": "L300", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_admin_api_filterdomainsbytenant", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L189", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_listrulebyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L135", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_listrules", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L247", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_quarantine", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L260", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_quarantinebyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L204", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_queue", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L217", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_queuebyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L67", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_userbyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L15", + "weight": 1.0, + "_origin": "ast", + "source": "internal_admin_handlers_go_admin_handler", + "target": "admin_handler_users", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L15", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_users", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_users", + "target": "db_userrole" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L15", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_users", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L15", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_users", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L189", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_listrulebyid", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L135", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_listrules", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L247", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_quarantine", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L260", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_quarantinebyid", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L204", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_queue", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L217", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_queuebyid", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L67", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_userbyid", + "target": "internal_admin_handlers_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L189", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_listrulebyid", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L135", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_listrules", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L247", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_quarantine", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L260", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_quarantinebyid", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L204", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_queue", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L217", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_queuebyid", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L67", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_userbyid", + "target": "internal_admin_handlers_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L67", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_userbyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/admin/handlers.go", + "source_location": "L176", + "weight": 1.0, + "_origin": "ast", + "source": "admin_handler_listrules", + "target": "db_listruleaction" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L135", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_listrules", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L189", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_listrulebyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L204", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_queue", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L217", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_queuebyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L247", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_quarantine", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/admin/handlers.go", + "source_location": "L260", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "admin_handler_quarantinebyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L20", + "weight": 1.0, + "_origin": "ast", + "source": "internal_auth_auth", + "target": "auth_scope", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_auth_auth", + "target": "internal_auth_auth_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "internal_auth_auth", + "target": "internal_auth_auth_checkapppassword", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L33", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_auth_auth_authenticate", + "target": "auth_scope", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L53", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_auth_auth_checkapppassword", + "target": "auth_scope", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_authenticate", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/session.go", + "source_location": "L198", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_authenticateuser", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L33", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_auth_auth_authenticate", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "internal_auth_auth_authenticate", + "target": "internal_auth_auth_checkapppassword", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L33", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_auth_auth_authenticate", + "target": "internal_auth_auth_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/smtp/auth.go", + "source_location": "L10", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_auth_authenticate", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/jmap/jmap.go", + "source_location": "L103", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_authenticate", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/managesieve/session.go", + "source_location": "L159", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdauthenticate", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/pop3/pop3.go", + "source_location": "L254", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdpass", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L115", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L746", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfadisable", + "target": "internal_auth_auth_authenticate" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/auth/auth.go", + "source_location": "L53", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_auth_auth_checkapppassword", + "target": "internal_auth_auth_go_db", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L12", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_config", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_databaseconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_jmapconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L114", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_linkedaccountsconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L82", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_notifyconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L101", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_oauthconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_oauthproviderconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_pipelineconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L90", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_pop3config", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L64", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_ratelimitconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_securityconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_serverconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L57", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_storageconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L42", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "config_tlsconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L164", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "internal_config_config_applyenvoverrides", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L206", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "internal_config_config_default", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "internal_config_config_load", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L188", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "internal_config_config_validate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L277", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config", + "target": "internal_config_config_writedefault", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L15", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_databaseconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L21", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_jmapconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_linkedaccountsconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L19", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_notifyconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L22", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_oauthconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L18", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_pipelineconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L20", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_pop3config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L17", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_ratelimitconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L24", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_securityconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L13", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_serverconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L16", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_storageconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L14", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_config", + "target": "config_tlsconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L164", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_config_config_applyenvoverrides", + "target": "config_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L206", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_config_config_default", + "target": "config_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L137", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_config_config_load", + "target": "config_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L188", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_config_config_validate", + "target": "config_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L152", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_verdictfor", + "target": "config_pipelineconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L103", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "config_oauthconfig", + "target": "config_oauthproviderconfig", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L155", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config_load", + "target": "internal_config_config_applyenvoverrides", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L150", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config_load", + "target": "internal_config_config_default", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L157", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config_load", + "target": "internal_config_config_validate", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L139", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config_load", + "target": "internal_config_config_writedefault", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/config/config.go", + "source_location": "L278", + "weight": 1.0, + "_origin": "ast", + "source": "internal_config_config_writedefault", + "target": "internal_config_config_default", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L49", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_decodekey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L107", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_decrypt", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L127", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_decryptwith", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L64", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_derivekey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_encrypt", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L160", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_generatemasterkeyhex", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_loadmasterkey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L146", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto", + "target": "internal_crypto_crypto_needsreencryption", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L38", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "dav_handler", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/bootstrap.go", + "source_location": "L16", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_bootstrap", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L107", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_crypto_crypto_decrypt", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L78", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_crypto_crypto_encrypt", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L33", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_crypto_crypto_loadmasterkey", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L146", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_crypto_crypto_needsreencryption", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L41", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_dav_dav_newhandler", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L32", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_mailstore_maildir_new", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L37", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_tlsutil_acme_manager_newacmemanager", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webmail_api_newhandler", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L28", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "mailstore_store", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L28", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L39", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_handler", + "target": "crypto_masterkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_loadmasterkey", + "target": "internal_crypto_crypto_decodekey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L128", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_decryptwith", + "target": "internal_crypto_crypto_derivekey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L79", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_encrypt", + "target": "internal_crypto_crypto_derivekey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L286", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/db/bootstrap.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_bootstrap", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/crypto/crypto.go", + "source_location": "L94", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_encrypt", + "target": "mailstore_store_read" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L45", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_deliver", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L203", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_writequarantinefile", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L166", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_writequeuefile", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L184", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_loadorcreateaccountkey", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_obtainandstore", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L669", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_crypto_crypto_encrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L366", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_reportcalendar", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L208", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_reportcontacts", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L258", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dav/dav.go", + "source_location": "L109", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_decrypt", + "target": "internal_crypto_crypto_decryptwith", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L100", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_read", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/mailstore/maildir.go", + "source_location": "L230", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_readquarantinefile", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_loadfromdb", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L163", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_loadorcreateaccountkey", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L701", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L176", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_crypto_crypto_decrypt" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/crypto/crypto.go", + "source_location": "L151", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_needsreencryption", + "target": "internal_crypto_crypto_decryptwith", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/crypto/crypto.go", + "source_location": "L162", + "weight": 1.0, + "_origin": "ast", + "source": "internal_crypto_crypto_generatemasterkeyhex", + "target": "mailstore_store_read" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L36", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "dav_handler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L421", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "dav_multistatusresponse", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L414", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "dav_propset", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "internal_dav_dav_newhandler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L385", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "internal_dav_dav_parsecollectionpath", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L428", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "internal_dav_dav_writemultistatus", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L467", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav", + "target": "internal_dav_dav_xmlescape", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L64", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L325", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_propfindcalendar", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L168", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_propfindcontacts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L352", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_reportcalendar", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L195", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_reportcontacts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L223", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_servecaldav", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L74", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_servecarddav", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L45", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler", + "target": "dav_handler_servehttp", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L37", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "dav_handler", + "target": "internal_dav_dav_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L41", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_dav_dav_newhandler", + "target": "dav_handler", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L41", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_dav_dav_newhandler", + "target": "internal_dav_dav_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servehttp", + "target": "dav_handler_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servehttp", + "target": "dav_handler_servecaldav", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L56", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servehttp", + "target": "dav_handler_servecarddav", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L45", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servehttp", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L45", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servehttp", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L325", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_propfindcalendar", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L168", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_propfindcontacts", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L352", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_reportcalendar", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L195", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_reportcontacts", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L223", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L428", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_dav_dav_writemultistatus", + "target": "internal_dav_dav_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L64", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_authenticate", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L325", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_propfindcalendar", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L168", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_propfindcontacts", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L352", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_reportcalendar", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L195", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_reportcontacts", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L223", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "internal_dav_dav_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L64", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "dav_handler_authenticate", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L94", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "dav_handler_propfindcontacts", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L97", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "dav_handler_reportcontacts", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L75", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecarddav", + "target": "internal_dav_dav_parsecollectionpath", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L168", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_propfindcontacts", + "target": "db_addressbook", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L192", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_propfindcontacts", + "target": "internal_dav_dav_writemultistatus", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L195", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_reportcontacts", + "target": "db_addressbook", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L218", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_reportcontacts", + "target": "internal_dav_dav_writemultistatus", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L243", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "dav_handler_propfindcalendar", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L246", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "dav_handler_reportcalendar", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L223", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L224", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_servecaldav", + "target": "internal_dav_dav_parsecollectionpath", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L325", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_propfindcalendar", + "target": "db_calendar", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L349", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_propfindcalendar", + "target": "internal_dav_dav_writemultistatus", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L352", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "dav_handler_reportcalendar", + "target": "db_calendar", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L376", + "weight": 1.0, + "_origin": "ast", + "source": "dav_handler_reportcalendar", + "target": "internal_dav_dav_writemultistatus", + "confidence_score": 1.0 + }, + { + "relation": "references", + "context": "return_type", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L385", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav_parsecollectionpath", + "target": "db_ownertype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L385", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_dav_dav_parsecollectionpath", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L423", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "dav_multistatusresponse", + "target": "dav_propset", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L428", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_dav_dav_writemultistatus", + "target": "dav_multistatusresponse", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dav/dav.go", + "source_location": "L435", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dav_dav_writemultistatus", + "target": "internal_dav_dav_xmlescape", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/bootstrap.go", + "source_location": "L16", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_bootstrap_go_db_db", + "target": "db_db_bootstrap", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/db/bootstrap.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_bootstrap", + "target": "internal_dkim_keys_generatekeypair" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L16", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db", + "target": "internal_db_db_go_db_db", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db", + "target": "internal_db_db_open", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db", + "target": "internal_db_db_registerdriver", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L132", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db_go_db_db", + "target": "db_db_insertmigrationsql", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L83", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db_go_db_db", + "target": "db_db_migrate", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L126", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db_go_db_db", + "target": "db_db_migrationapplied", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L138", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_db_go_db_db", + "target": "db_db_placeholder", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L26", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_db_db_open", + "target": "internal_db_db_go_db_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_migrate", + "target": "db_db_insertmigrationsql", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L91", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_migrate", + "target": "db_db_migrationapplied", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L128", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_migrationapplied", + "target": "db_db_placeholder", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/db.go", + "source_location": "L133", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_insertmigrationsql", + "target": "db_db_placeholder", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/migrations.go", + "source_location": "L7", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_migrations", + "target": "db_migration", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L274", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_addressbook", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L82", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_alias", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L61", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_apppassword", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L294", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_calendar", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L306", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_calendarobject", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L167", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_checkresult", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L284", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_contact", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L239", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L231", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_linkedaccountauthtype", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L222", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_linkedaccountprovider", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L100", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_listrule", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L93", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_listruleaction", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_message", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L177", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_messagecheck", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_messageverdict", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L347", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_mfabackupcode", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L151", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L267", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_ownertype", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L197", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_quarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L189", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_quarantinestatus", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L210", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_releasetoken", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_session", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L322", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_sievescript", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L7", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_tenant", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L334", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_tlscert", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L39", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_models", + "target": "db_userrole", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L475", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_createtenant", + "target": "db_tenant", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L456", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listtenants", + "target": "db_tenant", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L15", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_lookupdomain", + "target": "db_tenant", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L15", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_tenant", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L281", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_addressbook", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L69", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_apppassword", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L303", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_calendar", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L317", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_calendarobject", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L291", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_contact", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L26", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_domain", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L259", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_linkedaccount", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L108", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_listrule", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L146", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_mailboxentry", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L134", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_message", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L352", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_mfabackupcode", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L161", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_outboundqueueentry", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L207", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_quarantineentry", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L217", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_releasetoken", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L79", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_session", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L329", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_sievescript", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L342", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_tlscert", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L58", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_user", + "target": "internal_db_models_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L501", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_createdomain", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L517", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getdomain", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L482", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listdomains", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L15", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_lookupdomain", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1178", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_lookupdomainbyname", + "target": "db_domain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L46", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_user", + "target": "db_userrole", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L563", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_createuser", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L584", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getuser", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L536", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listusers", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L43", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_lookupuserbyemail", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L32", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/auth.go", + "source_location": "L9", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_smtp_auth_authenticate", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L98", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "jmap_handler_authenticate", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L156", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L177", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_mailboxget", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L27", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_session", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L137", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_session", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L50", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_recipienttarget", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L42", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L832", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_apppasswordbyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L759", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L498", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_deleteaccount", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L244", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_getme", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L472", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listaccounts", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L256", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listfolders", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L266", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listmessages", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L381", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listquarantine", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L320", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_messagebyid", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L685", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L733", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfadisable", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L659", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L545", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L252", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_provider", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L390", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_releasequarantine", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L291", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_sendorlistmessages", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L921", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_setrecoveryemail", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L437", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_sseevents", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L208", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_withauth", + "target": "db_user", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L68", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_lookupalias", + "target": "db_alias", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L88", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_matchlistrule", + "target": "db_listruleaction", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L103", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_listrule", + "target": "db_listruleaction", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L623", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_createlistrule", + "target": "db_listrule", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L604", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listlistrules", + "target": "db_listrule", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L139", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_updatemessageverdict", + "target": "db_messageverdict", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L130", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_message", + "target": "db_messageverdict", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L152", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_verdictfor", + "target": "db_messageverdict", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L31", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "db_messageverdict", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L560", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_session_quarantinemessage", + "target": "db_messageverdict", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L122", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertmessage", + "target": "db_message", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L33", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "db_message", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L41", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_mailcontext_parsedmessage", + "target": "db_message", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L288", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertmailboxentry", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L301", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listmailboxentries", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L234", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_session_sendfetchresponse", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L418", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_commands_matchessearch", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L487", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_commands_parseseqnum", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L43", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L143", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_session", + "target": "db_mailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1107", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_dueoutboundentries", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1094", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertoutboundqueueentry", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L636", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listalloutboundqueue", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1152", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_permanentlyfailedentries", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L117", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "queue_worker_attemptdelivery", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L188", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "queue_worker_bounce", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L161", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "queue_worker_scheduleretry", + "target": "db_outboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L181", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_messagecheck", + "target": "db_checkresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L32", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_spfoutcome", + "target": "db_checkresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L70", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_stageresult", + "target": "db_checkresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L146", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertmessagecheck", + "target": "db_messagecheck", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L201", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_quarantineentry", + "target": "db_quarantinestatus", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L210", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getquarantineentry", + "target": "db_quarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L160", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertquarantineentry", + "target": "db_quarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L672", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listallquarantine", + "target": "db_quarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L174", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_quarantineentriesforuser", + "target": "db_quarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L230", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertreleasetoken", + "target": "db_releasetoken", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L238", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_lookupreleasetoken", + "target": "db_releasetoken", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L242", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_linkedaccount", + "target": "db_linkedaccountprovider", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L245", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_linkedaccount", + "target": "db_linkedaccountauthtype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L410", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getlinkedaccount", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L361", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_insertlinkedaccount", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L374", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listlinkedaccounts", + "target": "db_linkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L276", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_addressbook", + "target": "db_ownertype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/models.go", + "source_location": "L296", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "db_calendar", + "target": "db_ownertype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L713", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_getorcreateaddressbook", + "target": "db_ownertype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L735", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_getorcreatecalendar", + "target": "db_ownertype", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L713", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getorcreateaddressbook", + "target": "db_addressbook", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L776", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getcontact", + "target": "db_contact", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L758", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listcontacts", + "target": "db_contact", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L793", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_upsertcontact", + "target": "db_contact", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L735", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getorcreatecalendar", + "target": "db_calendar", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L838", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getcalendarobject", + "target": "db_calendarobject", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L810", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listcalendarobjects", + "target": "db_calendarobject", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L862", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_upsertcalendarobject", + "target": "db_calendarobject", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L916", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getactivesievescript", + "target": "db_sievescript", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L900", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getsievescript", + "target": "db_sievescript", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L882", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_listsievescripts", + "target": "db_sievescript", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L930", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_upsertsievescript", + "target": "db_sievescript", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L971", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_gettlscert", + "target": "db_tlscert", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L986", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_upserttlscert", + "target": "db_tlscert", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L693", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries", + "target": "db_stats", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L11", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries", + "target": "internal_db_queries_uuidnew", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L727", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_getorcreateaddressbook", + "target": "internal_db_queries_uuidnew", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L750", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_getorcreatecalendar", + "target": "internal_db_queries_uuidnew", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1044", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_replacebackupcodes", + "target": "internal_db_queries_uuidnew", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1007", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_setacmeaccountkey", + "target": "internal_db_queries_uuidnew", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1026", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_cleartotpsecret", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1054", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_consumebackupcode", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L501", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_createdomain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L623", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_createlistrule", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L475", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_createtenant", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L563", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_createuser", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L447", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deactivatelinkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L875", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletecalendarobject", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L805", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletecontact", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L512", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletedomain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L629", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletelistrule", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L354", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletemailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1136", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deleteoutboundentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L667", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletequarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L964", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deletesievescript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L579", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_deleteuser", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1107", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_dueoutboundentries", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L916", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getactivesievescript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L838", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getcalendarobject", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L776", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getcontact", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L517", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getdomain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L410", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getlinkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L713", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getorcreateaddressbook", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L735", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getorcreatecalendar", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L210", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getquarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L900", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getsievescript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L701", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getstats", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L971", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_gettlscert", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L584", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_getuser", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L361", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertlinkedaccount", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L288", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertmailboxentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertmessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L146", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertmessagecheck", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1094", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertoutboundqueueentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L160", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertquarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L230", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_insertreleasetoken", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L636", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listalloutboundqueue", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L672", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listallquarantine", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L810", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listcalendarobjects", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L758", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listcontacts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L482", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listdomains", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L374", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listlinkedaccounts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L604", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listlistrules", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L301", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listmailboxentries", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L324", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listmailboxnames", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L882", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listsievescripts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L456", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listtenants", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L536", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_listusers", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L68", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_lookupalias", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L15", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_lookupdomain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1178", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_lookupdomainbyname", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L238", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_lookupreleasetoken", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_lookupuserbyemail", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L254", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_markreleasetokenused", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L88", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_matchlistrule", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L260", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_nextmailboxuid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1152", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_permanentlyfailedentries", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L174", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_quarantineentriesforuser", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L203", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_releasequarantineentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1034", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_replacebackupcodes", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1142", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_retryoutboundentry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L657", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_retryqueueentrynow", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1001", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setacmeaccountkey", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L945", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setactivesievescript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1021", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setmfaenabled", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1016", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setpendingtotpsecret", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1086", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setrecoveryemail", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L569", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setuseractive", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L574", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_setuserpassword", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L507", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_updatedomaindkimkey", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L441", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_updatelinkedaccountsync", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L347", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_updatemailboxflags", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L139", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_updatemessageverdict", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L862", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_upsertcalendarobject", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L793", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_upsertcontact", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L930", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_upsertsievescript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L986", + "weight": 1.0, + "_origin": "ast", + "source": "internal_db_queries_go_db_db", + "target": "db_db_upserttlscert", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1179", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_lookupdomainbyname", + "target": "db_db_lookupdomain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L139", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_updatemessageverdict", + "target": "internal_db_queries_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L174", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_quarantineentriesforuser", + "target": "internal_db_queries_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1142", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "db_db_retryoutboundentry", + "target": "internal_db_queries_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L701", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "db_db_getstats", + "target": "db_stats", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L794", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_upsertcontact", + "target": "db_db_getcontact", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L863", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_upsertcalendarobject", + "target": "db_db_getcalendarobject", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L931", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_upsertsievescript", + "target": "db_db_getsievescript", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L1002", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_setacmeaccountkey", + "target": "db_db_gettlscert", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/db/queries.go", + "source_location": "L987", + "weight": 1.0, + "_origin": "ast", + "source": "db_db_upserttlscert", + "target": "db_db_gettlscert", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L17", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys", + "target": "dkim_keypair", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys", + "target": "internal_dkim_keys_extractsignatureinfo", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L25", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys", + "target": "internal_dkim_keys_generatekeypair", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L85", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys", + "target": "internal_dkim_keys_parsednspublickey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys", + "target": "internal_dkim_keys_parseprivatekey", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L25", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_dkim_keys_generatekeypair", + "target": "dkim_keypair", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/keys.go", + "source_location": "L53", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_dkim_keys_parseprivatekey", + "target": "internal_dkim_keys_go_privatekey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/sign.go", + "source_location": "L25", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_keys_parseprivatekey" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/keys.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys_extractsignatureinfo", + "target": "internal_dkim_sign_parseheaders" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/keys.go", + "source_location": "L70", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys_extractsignatureinfo", + "target": "internal_dkim_sign_splitmessage" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/keys.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys_extractsignatureinfo", + "target": "internal_dkim_verify_parsedkimtags" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dkimstage_run", + "target": "internal_dkim_keys_extractsignatureinfo" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/keys.go", + "source_location": "L86", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_keys_parsednspublickey", + "target": "internal_dkim_verify_parsedkimtags" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dkimstage_run", + "target": "internal_dkim_keys_parsednspublickey" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L77", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_builddkimheader", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L167", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_canonicalizebodyrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L151", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_canonicalizeheaderrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L139", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_canonicalizeheadersrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L159", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_collapsewsp", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L103", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_parseheaders", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L24", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_sign", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L86", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign", + "target": "internal_dkim_sign_splitmessage", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_sign_builddkimheader", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_sign_canonicalizebodyrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_sign_canonicalizeheaderrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_sign_canonicalizeheadersrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L35", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_sign_parseheaders", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_sign", + "target": "internal_dkim_sign_splitmessage", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/queue/queue.go", + "source_location": "L128", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_attemptdelivery", + "target": "internal_dkim_sign_sign" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/verify.go", + "source_location": "L20", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_sign_splitmessage" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/verify.go", + "source_location": "L21", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_sign_parseheaders" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L146", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_canonicalizeheadersrelaxed", + "target": "internal_dkim_sign_canonicalizeheaderrelaxed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/verify.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_sign_canonicalizeheadersrelaxed" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/sign.go", + "source_location": "L153", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_sign_canonicalizeheaderrelaxed", + "target": "internal_dkim_sign_collapsewsp", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/verify.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_sign_canonicalizeheaderrelaxed" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/dkim/verify.go", + "source_location": "L37", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_sign_canonicalizebodyrelaxed" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify", + "target": "internal_dkim_verify_parsedkimtags", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify", + "target": "internal_dkim_verify_replacedkimtag", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L103", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify", + "target": "internal_dkim_verify_trimtrailingcrlf", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L19", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify", + "target": "internal_dkim_verify_verify", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L28", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_verify_parsedkimtags", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_verify_replacedkimtag", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/dkim/verify.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_dkim_verify_verify", + "target": "internal_dkim_verify_trimtrailingcrlf", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L17", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical", + "target": "ical_event", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L136", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical", + "target": "internal_ical_ical_escape", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical", + "target": "internal_ical_ical_parse", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L109", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical", + "target": "internal_ical_ical_splitproperty", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L144", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical", + "target": "internal_ical_ical_unescape", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical", + "target": "internal_ical_ical_unfold", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L82", + "weight": 1.0, + "_origin": "ast", + "source": "ical_event", + "target": "ical_event_build", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "ical_event", + "target": "internal_ical_ical_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L27", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_ical_ical_parse", + "target": "ical_event", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/ical/ical_fuzz_test.go", + "source_location": "L21", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical_fuzz_test_fuzzparse", + "target": "internal_ical_ical_parse" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical_parse", + "target": "internal_ical_ical_splitproperty", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical_parse", + "target": "internal_ical_ical_unescape", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L28", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical_parse", + "target": "internal_ical_ical_unfold", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "ical_event_build", + "target": "internal_ical_ical_escape", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ical_ical_fuzz_test", + "target": "internal_ical_ical_fuzz_test_fuzzparse", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ical/ical_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_ical_ical_fuzz_test_fuzzparse", + "target": "internal_ical_ical_fuzz_test_go_f", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L359", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_addflag", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L211", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_expandfetchitems", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L285", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_extractheaders", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L380", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_flagstoimap", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L293", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_indexof", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L418", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_matchessearch", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L487", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_parseseqnum", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L504", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_quoteifneeded", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L369", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands", + "target": "internal_imap_commands_removeflag", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L11", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdcapability", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L118", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdclose", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L129", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdexpunge", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L192", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdfetch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L99", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdlist", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdlogin", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L386", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdsearch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdselectexamine", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L22", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdstarttls", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L311", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmdstore", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L170", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_cmduid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L146", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_expungedeleted", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L453", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_resolvesequenceset", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L234", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_go_imap_session", + "target": "imap_session_sendfetchresponse", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdlist", + "target": "internal_imap_commands_quoteifneeded", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdclose", + "target": "imap_session_expungedeleted", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdexpunge", + "target": "imap_session_expungedeleted", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L180", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmduid", + "target": "imap_session_cmdfetch", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L184", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmduid", + "target": "imap_session_cmdsearch", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L182", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmduid", + "target": "imap_session_cmdstore", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L201", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdfetch", + "target": "imap_session_resolvesequenceset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L206", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdfetch", + "target": "imap_session_sendfetchresponse", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L202", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdfetch", + "target": "internal_imap_commands_expandfetchitems", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/commands.go", + "source_location": "L213", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_expandfetchitems", + "target": "internal_imap_parser_islist" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/commands.go", + "source_location": "L214", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_expandfetchitems", + "target": "internal_imap_parser_splitlist" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L273", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_sendfetchresponse", + "target": "internal_imap_commands_addflag", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L263", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_sendfetchresponse", + "target": "internal_imap_commands_extractheaders", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L242", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_sendfetchresponse", + "target": "internal_imap_commands_flagstoimap", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L287", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_commands_extractheaders", + "target": "internal_imap_commands_indexof", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L324", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdstore", + "target": "imap_session_resolvesequenceset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L338", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdstore", + "target": "internal_imap_commands_addflag", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L352", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdstore", + "target": "internal_imap_commands_flagstoimap", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L342", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdstore", + "target": "internal_imap_commands_removeflag", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/commands.go", + "source_location": "L326", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdstore", + "target": "internal_imap_parser_splitlist" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L397", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_cmdsearch", + "target": "internal_imap_commands_matchessearch", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/commands.go", + "source_location": "L461", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_resolvesequenceset", + "target": "internal_imap_commands_parseseqnum", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/parser.go", + "source_location": "L75", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_parser", + "target": "internal_imap_parser_islist", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/parser.go", + "source_location": "L66", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_parser", + "target": "internal_imap_parser_splitlist", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/parser.go", + "source_location": "L11", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_parser", + "target": "internal_imap_parser_tokenize", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/session.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_readcommand", + "target": "internal_imap_parser_tokenize" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/parser.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_parser_splitlist", + "target": "internal_imap_parser_tokenize", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/tokenize_fuzz_test.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_tokenize_fuzz_test_fuzztokenize", + "target": "internal_imap_parser_tokenize" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_server", + "target": "imap_server", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L136", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_server", + "target": "internal_imap_server_connhost", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_server", + "target": "internal_imap_server_newserver", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L84", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server", + "target": "imap_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L127", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server", + "target": "imap_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server", + "target": "imap_server_listenandserve", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L112", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server", + "target": "imap_server_shutdown", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L36", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imap_server", + "target": "internal_imap_server_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L34", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imap_server", + "target": "internal_imap_server_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L43", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imap_server", + "target": "internal_imap_server_go_limiter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L39", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imap_server", + "target": "internal_imap_server_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L41", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imap_server", + "target": "internal_imap_server_go_waitgroup", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L35", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imap_server", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L46", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_imap_server_newserver", + "target": "imap_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L46", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_server_newserver", + "target": "internal_imap_server_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L46", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_server_newserver", + "target": "internal_imap_server_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L84", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_server_acceptloop", + "target": "internal_imap_server_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L46", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_server_newserver", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server_listenandserve", + "target": "imap_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L64", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server_listenandserve", + "target": "imap_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L52", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_server_listenandserve", + "target": "internal_imap_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L84", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_server_acceptloop", + "target": "internal_imap_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server_acceptloop", + "target": "internal_imap_server_connhost", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/imap/server.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server_acceptloop", + "target": "internal_imap_session_newsession" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "imap_server_shutdown", + "target": "imap_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L112", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_server_shutdown", + "target": "internal_imap_server_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/server.go", + "source_location": "L136", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_server_connhost", + "target": "internal_imap_server_go_addr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session", + "target": "imap_state", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session", + "target": "internal_imap_session_go_imap_session", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session", + "target": "internal_imap_session_newsession", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L31", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_state", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L197", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_authenticateuser", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L181", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_continuation", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L88", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_readcommand", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L153", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_requireauthenticated", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L161", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_requireselected", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L57", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_run", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L171", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_tagged", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L176", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_untagged", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L186", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "imap_session_upgradetls", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L27", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "internal_imap_session_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L28", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "internal_imap_session_go_readwriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L29", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_imap_session_go_imap_session", + "target": "internal_imap_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L46", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_imap_session_newsession", + "target": "internal_imap_session_go_imap_session", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L46", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_session_newsession", + "target": "internal_imap_session_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L187", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_upgradetls", + "target": "internal_imap_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L46", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_session_newsession", + "target": "internal_imap_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_run", + "target": "imap_session_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_run", + "target": "imap_session_readcommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_run", + "target": "imap_session_untagged", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L57", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_session_run", + "target": "internal_imap_session_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_dispatch", + "target": "imap_session_tagged", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L115", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_dispatch", + "target": "imap_session_untagged", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L155", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_requireauthenticated", + "target": "imap_session_tagged", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L163", + "weight": 1.0, + "_origin": "ast", + "source": "imap_session_requireselected", + "target": "imap_session_tagged", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/session.go", + "source_location": "L186", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imap_session_upgradetls", + "target": "internal_imap_session_go_config", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imap/tokenize_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imap_tokenize_fuzz_test", + "target": "internal_imap_tokenize_fuzz_test_fuzztokenize", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imap/tokenize_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imap_tokenize_fuzz_test_fuzztokenize", + "target": "internal_imap_tokenize_fuzz_test_go_f", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L21", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "imapclient_client", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L142", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "imapclient_fetchedmessage", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L87", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "imapclient_folderinfo", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L118", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "imapclient_selectedinfo", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "internal_imapclient_client_dial", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L262", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "internal_imapclient_client_parsefetchlines", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L291", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client", + "target": "internal_imapclient_client_quote", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L193", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L185", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_expunge", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L151", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_fetch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L91", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_list", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L60", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_login", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_loginxoauth2", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L81", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_logout", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L232", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_readline", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_select", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L213", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "context": "call", + "source": "imapclient_client_starttls", + "target": "imapclient_client", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L176", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_store", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L165", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_uidfetch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L181", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client", + "target": "imapclient_client_uidstore", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L22", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imapclient_client", + "target": "internal_imapclient_client_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L24", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imapclient_client", + "target": "internal_imapclient_client_go_writer", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "imapclient_client", + "target": "reader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "context": "return_type", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client_dial", + "target": "imapclient_client", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "internal_imapclient_client_dial", + "target": "imapclient_client_readline", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L31", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imapclient_client_dial", + "target": "internal_imapclient_client_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L31", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_imapclient_client_dial", + "target": "internal_imapclient_client_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L49", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "imapclient_client_starttls", + "target": "internal_imapclient_client_go_config", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_starttls", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L61", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_login", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L61", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_login", + "target": "internal_imapclient_client_quote", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_loginxoauth2", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L82", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_logout", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L91", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "imapclient_client_list", + "target": "imapclient_folderinfo", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_list", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L122", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "imapclient_client_select", + "target": "imapclient_selectedinfo", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_select", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_select", + "target": "internal_imapclient_client_quote", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L151", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "imapclient_client_fetch", + "target": "imapclient_fetchedmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L165", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "imapclient_client_uidfetch", + "target": "imapclient_fetchedmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L262", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_imapclient_client_parsefetchlines", + "target": "imapclient_fetchedmessage", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L152", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_fetch", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L159", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_fetch", + "target": "internal_imapclient_client_parsefetchlines", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L166", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_uidfetch", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L173", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_uidfetch", + "target": "internal_imapclient_client_parsefetchlines", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L177", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_store", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L182", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_uidstore", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L186", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_expunge", + "target": "imapclient_client_simplecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L202", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_command", + "target": "imapclient_client_readline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/imapclient/client.go", + "source_location": "L214", + "weight": 1.0, + "_origin": "ast", + "source": "imapclient_client_simplecommand", + "target": "imapclient_client_command", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L202", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "internal_jmap_jmap_jmaprole", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "internal_jmap_jmap_newhandler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L282", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "internal_jmap_jmap_splitcompositeid", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L290", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "internal_jmap_jmap_truncatepreview", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L302", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "internal_jmap_jmap_writejson", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "jmap_handler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L151", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "jmap_methodresult", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L108", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "jmap_request", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "internal_jmap_jmap", + "target": "jmap_response", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L40", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_jmap_jmap_newhandler", + "target": "jmap_handler", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L35", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "jmap_handler", + "target": "internal_jmap_jmap_go_db", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L118", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_api", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L156", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L248", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_emailget", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L221", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_emailquery", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L177", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_mailboxget", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L44", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_registerroutes", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler", + "target": "jmap_handler_session", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L36", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "jmap_handler", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L40", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_jmap_jmap_newhandler", + "target": "internal_jmap_jmap_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L40", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_jmap_jmap_newhandler", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L44", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_registerroutes", + "target": "internal_jmap_jmap_go_servemux", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L51", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_session", + "target": "internal_jmap_jmap_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L51", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_session", + "target": "internal_jmap_jmap_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L95", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_session", + "target": "internal_jmap_jmap_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_session", + "target": "jmap_handler_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L302", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_jmap_jmap_writejson", + "target": "internal_jmap_jmap_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L118", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_api", + "target": "internal_jmap_jmap_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L98", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_authenticate", + "target": "internal_jmap_jmap_go_request", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L119", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_api", + "target": "jmap_handler_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L118", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_api", + "target": "jmap_request", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L144", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_api", + "target": "internal_jmap_jmap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L148", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_api", + "target": "internal_jmap_jmap_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L144", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_api", + "target": "jmap_handler_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L156", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "jmap_methodresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L248", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "jmap_handler_emailget", + "target": "jmap_methodresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L221", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "jmap_handler_emailquery", + "target": "jmap_methodresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L177", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "jmap_handler_mailboxget", + "target": "jmap_methodresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L156", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "internal_jmap_jmap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L168", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "jmap_handler_emailget", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L165", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "jmap_handler_emailquery", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L162", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_dispatch", + "target": "jmap_handler_mailboxget", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L248", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_emailget", + "target": "internal_jmap_jmap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L221", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_emailquery", + "target": "internal_jmap_jmap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L177", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "jmap_handler_mailboxget", + "target": "internal_jmap_jmap_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L188", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_mailboxget", + "target": "internal_jmap_jmap_jmaprole", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L255", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_emailget", + "target": "internal_jmap_jmap_splitcompositeid", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/jmap/jmap.go", + "source_location": "L273", + "weight": 1.0, + "_origin": "ast", + "source": "jmap_handler_emailget", + "target": "internal_jmap_jmap_truncatepreview", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L139", + "weight": 1.0, + "_origin": "ast", + "source": "internal_mailstore_maildir", + "target": "internal_mailstore_maildir_maildirfilename", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L145", + "weight": 1.0, + "_origin": "ast", + "source": "internal_mailstore_maildir", + "target": "internal_mailstore_maildir_messageidfromfilename", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L32", + "weight": 1.0, + "_origin": "ast", + "source": "internal_mailstore_maildir", + "target": "internal_mailstore_maildir_new", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L125", + "weight": 1.0, + "_origin": "ast", + "source": "internal_mailstore_maildir", + "target": "internal_mailstore_maildir_sanitizepathcomponent", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_mailstore_maildir", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L32", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_mailstore_maildir_new", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L41", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newserver", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L48", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_queue_queue_newworker", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_server_newserver", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webmail_api_newhandler", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L29", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "mailstore_store", + "target": "internal_mailstore_maildir_go_db", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L188", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_deletequeuefile", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L39", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_deliver", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L107", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_ensuremailboxdirs", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L117", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_mailboxdir", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L93", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_read", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L235", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_readat", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L225", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_readquarantinefile", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L197", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_writequarantinefile", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L159", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store", + "target": "mailstore_store_writequeuefile", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L32", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_server", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L42", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "queue_worker", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L45", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L38", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_handler", + "target": "mailstore_store", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L32", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_mailstore_maildir_new", + "target": "internal_mailstore_maildir_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_deliver", + "target": "internal_mailstore_maildir_maildirfilename", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_deliver", + "target": "mailstore_store_ensuremailboxdirs", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_deliver", + "target": "mailstore_store_mailboxdir", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L141", + "weight": 1.0, + "_origin": "ast", + "source": "internal_mailstore_maildir_maildirfilename", + "target": "mailstore_store_read", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/totp/totp.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp_generatesecret", + "target": "mailstore_store_read" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L643", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api_randomstate", + "target": "mailstore_store_read" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L99", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_read", + "target": "internal_mailstore_maildir_messageidfromfilename", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L236", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_readat", + "target": "mailstore_store_read", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L798", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "mailstore_store_read" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L716", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "mailstore_store_read" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L108", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_ensuremailboxdirs", + "target": "mailstore_store_mailboxdir", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L118", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_mailboxdir", + "target": "internal_mailstore_maildir_sanitizepathcomponent", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L171", + "weight": 1.0, + "_origin": "ast", + "source": "mailstore_store_writequeuefile", + "target": "internal_mailstore_maildir_maildirfilename", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/mailstore/maildir.go", + "source_location": "L235", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "mailstore_store_readat", + "target": "internal_mailstore_maildir_go_writer", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_server", + "target": "internal_managesieve_server_newserver", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L23", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_server", + "target": "managesieve_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L33", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_managesieve_server_newserver", + "target": "managesieve_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L25", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_server", + "target": "internal_managesieve_server_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L24", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_server", + "target": "internal_managesieve_server_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L28", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_server", + "target": "internal_managesieve_server_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L30", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_server", + "target": "internal_managesieve_server_go_waitgroup", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L55", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_server", + "target": "managesieve_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L37", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_server", + "target": "managesieve_server_listenandserve", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L75", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_server", + "target": "managesieve_server_shutdown", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L33", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_managesieve_server_newserver", + "target": "internal_managesieve_server_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L33", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_managesieve_server_newserver", + "target": "internal_managesieve_server_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L55", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "managesieve_server_acceptloop", + "target": "internal_managesieve_server_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L37", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "managesieve_server_listenandserve", + "target": "internal_managesieve_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_server_listenandserve", + "target": "managesieve_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L55", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "managesieve_server_acceptloop", + "target": "internal_managesieve_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/managesieve/server.go", + "source_location": "L70", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_server_acceptloop", + "target": "internal_managesieve_session_newsession" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/server.go", + "source_location": "L75", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "managesieve_server_shutdown", + "target": "internal_managesieve_server_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L357", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_session", + "target": "internal_managesieve_session_escapequoted", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_session", + "target": "internal_managesieve_session_newsession", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L324", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_session", + "target": "internal_managesieve_session_splitquotedargs", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L312", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_session", + "target": "internal_managesieve_session_splitverb", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L22", + "weight": 1.0, + "_origin": "ast", + "source": "internal_managesieve_session", + "target": "managesieve_session", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L30", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_managesieve_session_newsession", + "target": "managesieve_session", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_session", + "target": "internal_managesieve_session_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L24", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_session", + "target": "internal_managesieve_session_go_readwriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L25", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "managesieve_session", + "target": "internal_managesieve_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmdauthenticate", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L262", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmddeletescript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L208", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmdgetscript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L225", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmdlistscripts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L177", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmdputscript", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L244", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmdsetactive", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L105", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_cmdstarttls", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L75", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L281", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L292", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_readliteralfromremainder", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L168", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_requireauth", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_run", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L65", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_sendcapabilities", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L276", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L30", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_managesieve_session_newsession", + "target": "internal_managesieve_session_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L30", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_managesieve_session_newsession", + "target": "internal_managesieve_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L111", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdstarttls", + "target": "internal_managesieve_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L40", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "managesieve_session_run", + "target": "internal_managesieve_session_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_run", + "target": "managesieve_session_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_run", + "target": "managesieve_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_run", + "target": "managesieve_session_sendcapabilities", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_run", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L79", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_sendcapabilities", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L66", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_sendcapabilities", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "internal_managesieve_session_splitverb", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L83", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmdauthenticate", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmddeletescript", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L90", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmdgetscript", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmdlistscripts", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L88", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmdputscript", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L94", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmdsetactive", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L81", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_cmdstarttls", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L85", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_dispatch", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L107", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdstarttls", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L129", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdauthenticate", + "target": "internal_managesieve_session_splitquotedargs", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L140", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdauthenticate", + "target": "managesieve_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L125", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdauthenticate", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L263", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmddeletescript", + "target": "managesieve_session_requireauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L209", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdgetscript", + "target": "managesieve_session_requireauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L226", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdlistscripts", + "target": "managesieve_session_requireauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L178", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdputscript", + "target": "managesieve_session_requireauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L245", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdsetactive", + "target": "managesieve_session_requireauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L170", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_requireauth", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L195", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdputscript", + "target": "internal_managesieve_session_escapequoted", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L181", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdputscript", + "target": "internal_managesieve_session_splitquotedargs", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L188", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdputscript", + "target": "managesieve_session_readliteralfromremainder", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L183", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdputscript", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L215", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdgetscript", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L231", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdlistscripts", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L252", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmdsetactive", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/managesieve/session.go", + "source_location": "L268", + "weight": 1.0, + "_origin": "ast", + "source": "managesieve_session_cmddeletescript", + "target": "managesieve_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L150", + "weight": 1.0, + "_origin": "ast", + "source": "internal_oauth2_oauth2", + "target": "internal_oauth2_oauth2_truncate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L38", + "weight": 1.0, + "_origin": "ast", + "source": "internal_oauth2_oauth2", + "target": "internal_oauth2_oauth2_wellknownendpoints", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L161", + "weight": 1.0, + "_origin": "ast", + "source": "internal_oauth2_oauth2", + "target": "internal_oauth2_oauth2_xoauth2saslstring", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L24", + "weight": 1.0, + "_origin": "ast", + "source": "internal_oauth2_oauth2", + "target": "oauth2_config", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "internal_oauth2_oauth2", + "target": "oauth2_token", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "internal_oauth2_oauth2", + "target": "oauth2_tokenresponse", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config", + "target": "oauth2_config_buildauthurl", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L112", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config", + "target": "oauth2_config_dotokenrequest", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L85", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config", + "target": "oauth2_config_exchangecode", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config", + "target": "oauth2_config_refreshtoken", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L112", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "oauth2_config_dotokenrequest", + "target": "oauth2_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L85", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "oauth2_config_exchangecode", + "target": "oauth2_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L96", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "oauth2_config_refreshtoken", + "target": "oauth2_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L55", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "oauth2_token", + "target": "internal_oauth2_oauth2_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L85", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "oauth2_config_exchangecode", + "target": "internal_oauth2_oauth2_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config_exchangecode", + "target": "oauth2_config_dotokenrequest", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L112", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "oauth2_config_dotokenrequest", + "target": "internal_oauth2_oauth2_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L96", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "oauth2_config_refreshtoken", + "target": "internal_oauth2_oauth2_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L102", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config_refreshtoken", + "target": "oauth2_config_dotokenrequest", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L133", + "weight": 1.0, + "_origin": "ast", + "source": "oauth2_config_dotokenrequest", + "target": "internal_oauth2_oauth2_truncate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/oauth2/oauth2.go", + "source_location": "L112", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "oauth2_config_dotokenrequest", + "target": "values", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "internal_pipeline_pipeline_defaultstages", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "internal_pipeline_pipeline_domainof", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "internal_pipeline_pipeline_neworchestrator", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L112", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "internal_pipeline_pipeline_stagesfromconfig", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L152", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "internal_pipeline_pipeline_verdictfor", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L22", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L84", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "pipeline_orchestrator", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "pipeline_stage", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L68", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L28", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_clamavstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L17", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_dkimstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L16", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L15", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_headerstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L62", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_llmstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "internal_pipeline_pipeline_go_ip", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L55", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "pipeline_mailcontext_mailfromdomain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "pipeline_mailcontext_parsedmessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "pipeline_mailcontext_rcptdomain", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L29", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_mailcontext", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L137", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_orchestrator_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L39", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_rspamdstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L16", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_spfstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L26", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_urlstage_run", + "target": "pipeline_mailcontext", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_mailcontext_rcptdomain", + "target": "internal_pipeline_pipeline_domainof", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L56", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_mailcontext_mailfromdomain", + "target": "internal_pipeline_pipeline_domainof", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L624", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_session_injectspamheaders", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L28", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_clamavstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L17", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_dkimstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L16", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L15", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_headerstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L62", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_llmstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L39", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_rspamdstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L16", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_spfstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L26", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_urlstage_run", + "target": "pipeline_stageresult", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L96", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_defaultstages", + "target": "pipeline_stage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L89", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_neworchestrator", + "target": "pipeline_stage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L112", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_stagesfromconfig", + "target": "pipeline_stage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L85", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_orchestrator", + "target": "pipeline_stage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L89", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_neworchestrator", + "target": "pipeline_orchestrator", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_server_newserver", + "target": "pipeline_orchestrator", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L86", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_orchestrator", + "target": "internal_pipeline_pipeline_go_config", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_orchestrator", + "target": "pipeline_orchestrator_run", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L47", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "pipeline_orchestrator", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L89", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_neworchestrator", + "target": "internal_pipeline_pipeline_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L112", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_pipeline_stagesfromconfig", + "target": "internal_pipeline_pipeline_go_config", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_pipeline_stagesfromconfig", + "target": "internal_pipeline_pipeline_defaultstages", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L137", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_orchestrator_run", + "target": "internal_pipeline_pipeline_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/pipeline.go", + "source_location": "L149", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_orchestrator_run", + "target": "internal_pipeline_pipeline_verdictfor", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L118", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_clamav", + "target": "internal_pipeline_stage_clamav_parseclamaddr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L21", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_clamav", + "target": "pipeline_clamavstage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L23", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_clamavstage", + "target": "internal_pipeline_stage_clamav_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_clamavstage", + "target": "pipeline_clamavstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L28", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_clamavstage", + "target": "pipeline_clamavstage_run", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L55", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_clamavstage", + "target": "pipeline_clamavstage_scan", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_clamavstage_run", + "target": "pipeline_clamavstage_name", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L28", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_clamavstage_run", + "target": "internal_pipeline_stage_clamav_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L32", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_clamavstage_run", + "target": "pipeline_clamavstage_scan", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L55", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_clamavstage_scan", + "target": "internal_pipeline_stage_clamav_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_clamav.go", + "source_location": "L56", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_clamavstage_scan", + "target": "internal_pipeline_stage_clamav_parseclamaddr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L13", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_dkim", + "target": "pipeline_dkimstage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L15", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dkimstage", + "target": "pipeline_dkimstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L17", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dkimstage", + "target": "pipeline_dkimstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L20", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dkimstage_run", + "target": "pipeline_dkimstage_name", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dkim.go", + "source_location": "L17", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_dkimstage_run", + "target": "internal_pipeline_stage_dkim_go_context", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_dmarc", + "target": "internal_pipeline_stage_dmarc_dmarctag", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_dmarc", + "target": "internal_pipeline_stage_dmarc_extractdomainfromheader", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_dmarc", + "target": "internal_pipeline_stage_dmarc_orgdomain", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L12", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_dmarc", + "target": "pipeline_dmarcstage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dmarcstage", + "target": "pipeline_dmarcstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L16", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dmarcstage", + "target": "pipeline_dmarcstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L19", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "pipeline_dmarcstage_name", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "internal_pipeline_stage_dmarc_dmarctag", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L24", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "internal_pipeline_stage_dmarc_extractdomainfromheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L16", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "internal_pipeline_stage_dmarc_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_dmarc.go", + "source_location": "L35", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_dmarcstage_run", + "target": "internal_pipeline_stage_dmarc_orgdomain", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L38", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_headerstage_run", + "target": "internal_pipeline_stage_dmarc_extractdomainfromheader" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L11", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_headers", + "target": "pipeline_headerstage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L13", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_headerstage", + "target": "pipeline_headerstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L15", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_headerstage", + "target": "pipeline_headerstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_headerstage_run", + "target": "pipeline_headerstage_name", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_headers.go", + "source_location": "L15", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_headerstage_run", + "target": "internal_pipeline_stage_headers_go_context", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L149", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_llm", + "target": "internal_pipeline_stage_llm_extractleadingdigits", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_llm", + "target": "pipeline_chatcompletionrequest", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_llm", + "target": "pipeline_chatcompletionresponse", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_llm", + "target": "pipeline_chatmessage", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L28", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_llm", + "target": "pipeline_llmstage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L31", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_llmstage", + "target": "internal_pipeline_stage_llm_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L93", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_llmstage", + "target": "pipeline_llmstage_classify", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_llmstage", + "target": "pipeline_llmstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L62", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_llmstage", + "target": "pipeline_llmstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L64", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_llmstage_run", + "target": "pipeline_llmstage_name", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L43", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_chatcompletionrequest", + "target": "pipeline_chatmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L52", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_chatcompletionresponse", + "target": "pipeline_chatmessage", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L62", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_llmstage_run", + "target": "internal_pipeline_stage_llm_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_llmstage_run", + "target": "pipeline_llmstage_classify", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L93", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_llmstage_classify", + "target": "internal_pipeline_stage_llm_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_llm.go", + "source_location": "L132", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_llmstage_classify", + "target": "internal_pipeline_stage_llm_extractleadingdigits", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_rspamd", + "target": "pipeline_rspamdresponse", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L19", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_rspamd", + "target": "pipeline_rspamdstage", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_rspamd", + "target": "pipeline_rspamdsymbol", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L21", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_rspamdstage", + "target": "internal_pipeline_stage_rspamd_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_rspamdstage", + "target": "pipeline_rspamdstage_check", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L24", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_rspamdstage", + "target": "pipeline_rspamdstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L39", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_rspamdstage", + "target": "pipeline_rspamdstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_rspamdstage_run", + "target": "pipeline_rspamdstage_name", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L30", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pipeline_rspamdresponse", + "target": "pipeline_rspamdsymbol", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L69", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "pipeline_rspamdstage_check", + "target": "pipeline_rspamdresponse", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L39", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_rspamdstage_run", + "target": "internal_pipeline_stage_rspamd_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_rspamdstage_run", + "target": "pipeline_rspamdstage_check", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_rspamd.go", + "source_location": "L69", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_rspamdstage_check", + "target": "internal_pipeline_stage_rspamd_go_context", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L36", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf", + "target": "internal_pipeline_stage_spf_checkspf", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L73", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf", + "target": "internal_pipeline_stage_spf_evaluatespf", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L153", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf", + "target": "internal_pipeline_stage_spf_matchcidr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf", + "target": "pipeline_spfoutcome", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L12", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf", + "target": "pipeline_spfstage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_spfstage", + "target": "pipeline_spfstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L16", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_spfstage", + "target": "pipeline_spfstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_spfstage_run", + "target": "pipeline_spfstage_name", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_spfstage_run", + "target": "internal_pipeline_stage_spf_checkspf", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L16", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_spfstage_run", + "target": "internal_pipeline_stage_spf_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L36", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_stage_spf_checkspf", + "target": "internal_pipeline_stage_spf_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L73", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_stage_spf_evaluatespf", + "target": "internal_pipeline_stage_spf_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L36", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pipeline_stage_spf_checkspf", + "target": "pipeline_spfoutcome", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf_checkspf", + "target": "internal_pipeline_stage_spf_evaluatespf", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L36", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_stage_spf_checkspf", + "target": "internal_pipeline_stage_spf_go_ip", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L73", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_stage_spf_evaluatespf", + "target": "internal_pipeline_stage_spf_go_ip", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L153", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pipeline_stage_spf_matchcidr", + "target": "internal_pipeline_stage_spf_go_ip", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_spf.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_spf_evaluatespf", + "target": "internal_pipeline_stage_spf_matchcidr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_url", + "target": "internal_pipeline_stage_url_dedupe", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L12", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pipeline_stage_url", + "target": "pipeline_urlstage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_urlstage", + "target": "pipeline_urlstage_name", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_urlstage", + "target": "pipeline_urlstage_run", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_urlstage_run", + "target": "pipeline_urlstage_name", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L81", + "weight": 1.0, + "_origin": "ast", + "source": "pipeline_urlstage_run", + "target": "internal_pipeline_stage_url_dedupe", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pipeline/stage_url.go", + "source_location": "L26", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pipeline_urlstage_run", + "target": "internal_pipeline_stage_url_go_context", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pop3_pop3", + "target": "internal_pop3_pop3_newserver", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L147", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pop3_pop3", + "target": "internal_pop3_pop3_newsession", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L122", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pop3_pop3", + "target": "pop3_pop3state", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pop3_pop3", + "target": "pop3_server", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L130", + "weight": 1.0, + "_origin": "ast", + "source": "internal_pop3_pop3", + "target": "pop3_session", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L41", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newserver", + "target": "pop3_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L147", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newsession", + "target": "pop3_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L33", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_server", + "target": "internal_pop3_pop3_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L31", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_server", + "target": "internal_pop3_pop3_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L36", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_server", + "target": "internal_pop3_pop3_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L38", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_server", + "target": "internal_pop3_pop3_go_waitgroup", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L77", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server", + "target": "pop3_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server", + "target": "pop3_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L45", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server", + "target": "pop3_server_listenandserve", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server", + "target": "pop3_server_shutdown", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L133", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L41", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newserver", + "target": "internal_pop3_pop3_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L41", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newserver", + "target": "internal_pop3_pop3_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L77", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pop3_server_acceptloop", + "target": "internal_pop3_pop3_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L45", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pop3_server_listenandserve", + "target": "internal_pop3_pop3_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server_listenandserve", + "target": "pop3_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L57", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server_listenandserve", + "target": "pop3_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L77", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pop3_server_acceptloop", + "target": "internal_pop3_pop3_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L159", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pop3_session_run", + "target": "internal_pop3_pop3_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server_acceptloop", + "target": "internal_pop3_pop3_newsession", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L93", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server_acceptloop", + "target": "pop3_session_run", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L98", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "pop3_server_shutdown", + "target": "internal_pop3_pop3_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L99", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_server_shutdown", + "target": "pop3_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L135", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_pop3state", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L147", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newsession", + "target": "pop3_session", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L131", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_session", + "target": "internal_pop3_pop3_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L132", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "pop3_session", + "target": "internal_pop3_pop3_go_readwriter", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L404", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmddele", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L291", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmdlist", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L249", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmdpass", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L341", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmdretr", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L416", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmdrset", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L275", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmdstat", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L359", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmdtop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L316", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmduidl", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L240", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_cmduser", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L427", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_commitdeletes", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L186", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L462", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_livecount", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L225", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L441", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L159", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_run", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L449", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_validmessagenum", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L475", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session", + "target": "pop3_session_writedotstuffed", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L147", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_pop3_pop3_newsession", + "target": "internal_pop3_pop3_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L180", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_run", + "target": "pop3_session_dispatch", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L160", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_run", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L214", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmddele", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L206", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmdlist", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L202", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmdpass", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L210", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmdretr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L216", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmdrset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L204", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmdstat", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L212", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmdtop", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L208", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmduidl", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L200", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_cmduser", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L196", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_commitdeletes", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L197", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_dispatch", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L413", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmddele", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L298", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdlist", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L251", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdpass", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L352", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdretr", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L421", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdrset", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L288", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdstat", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L365", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdtop", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L323", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmduidl", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L242", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmduser", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L443", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_requiretransaction", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L452", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_validmessagenum", + "target": "pop3_session_reply", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L276", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdstat", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L305", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdlist", + "target": "pop3_session_livecount", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L292", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdlist", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L317", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmduidl", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L342", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdretr", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L345", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdretr", + "target": "pop3_session_validmessagenum", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L356", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdretr", + "target": "pop3_session_writedotstuffed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L360", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdtop", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L368", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdtop", + "target": "pop3_session_validmessagenum", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L401", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdtop", + "target": "pop3_session_writedotstuffed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L405", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmddele", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L408", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmddele", + "target": "pop3_session_validmessagenum", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/pop3/pop3.go", + "source_location": "L417", + "weight": 1.0, + "_origin": "ast", + "source": "pop3_session_cmdrset", + "target": "pop3_session_requiretransaction", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L172", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "internal_queue_queue_backoffduration", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L220", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "internal_queue_queue_domainof", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L231", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "internal_queue_queue_ispermanenterror", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L320", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "internal_queue_queue_lookupmxhosts", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "internal_queue_queue_newworker", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L29", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "queue_deliverer", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L37", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "queue_keylookup", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L252", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "queue_mxdeliverer", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "internal_queue_queue", + "target": "queue_worker", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L43", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "queue_worker", + "target": "queue_deliverer", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L58", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "queue_worker_withdeliverer", + "target": "queue_deliverer", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L44", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "queue_worker", + "target": "queue_keylookup", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L67", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "queue_worker_withkeylookup", + "target": "queue_keylookup", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L48", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_queue_queue_newworker", + "target": "queue_worker", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L41", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "queue_worker", + "target": "internal_queue_queue_go_db", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L117", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker", + "target": "queue_worker_attemptdelivery", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L188", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker", + "target": "queue_worker_bounce", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker", + "target": "queue_worker_processonce", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L73", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker", + "target": "queue_worker_run", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L161", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker", + "target": "queue_worker_scheduleretry", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L90", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker", + "target": "queue_worker_stop", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "context": "return_type", + "source": "queue_worker_withdeliverer", + "target": "queue_worker", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L67", + "weight": 1.0, + "_origin": "ast", + "context": "return_type", + "source": "queue_worker_withkeylookup", + "target": "queue_worker", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L48", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_queue_queue_newworker", + "target": "internal_queue_queue_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_run", + "target": "queue_worker_processonce", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L75", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_run", + "target": "queue_worker_stop", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L104", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_processonce", + "target": "queue_worker_attemptdelivery", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L113", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_processonce", + "target": "queue_worker_bounce", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L126", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_attemptdelivery", + "target": "internal_queue_queue_domainof", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L146", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_attemptdelivery", + "target": "internal_queue_queue_ispermanenterror", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_attemptdelivery", + "target": "queue_mxdeliverer_deliver", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L121", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_attemptdelivery", + "target": "queue_worker_scheduleretry", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L162", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_scheduleretry", + "target": "internal_queue_queue_backoffduration", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L172", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_queue_queue_backoffduration", + "target": "internal_queue_queue_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L207", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_bounce", + "target": "internal_queue_queue_domainof", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L211", + "weight": 1.0, + "_origin": "ast", + "source": "queue_worker_bounce", + "target": "queue_mxdeliverer_deliver", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L257", + "weight": 1.0, + "_origin": "ast", + "source": "queue_mxdeliverer_deliver", + "target": "internal_queue_queue_domainof", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L256", + "weight": 1.0, + "_origin": "ast", + "source": "queue_mxdeliverer", + "target": "queue_mxdeliverer_deliver", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L274", + "weight": 1.0, + "_origin": "ast", + "source": "queue_mxdeliverer", + "target": "queue_mxdeliverer_delivertohost", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L258", + "weight": 1.0, + "_origin": "ast", + "source": "queue_mxdeliverer_deliver", + "target": "internal_queue_queue_lookupmxhosts", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/queue/queue.go", + "source_location": "L265", + "weight": 1.0, + "_origin": "ast", + "source": "queue_mxdeliverer_deliver", + "target": "queue_mxdeliverer_delivertohost", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/http.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_http", + "target": "internal_ratelimit_http_clientip", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/http.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_http_go_ratelimit_limiter", + "target": "ratelimit_limiter_httpmiddleware", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/http.go", + "source_location": "L14", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "ratelimit_limiter_httpmiddleware", + "target": "handler", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/http.go", + "source_location": "L16", + "weight": 1.0, + "_origin": "ast", + "source": "ratelimit_limiter_httpmiddleware", + "target": "internal_ratelimit_http_clientip", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/http.go", + "source_location": "L26", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_ratelimit_http_clientip", + "target": "internal_ratelimit_http_go_request", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L24", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit", + "target": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L36", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit", + "target": "internal_ratelimit_ratelimit_new", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit", + "target": "ratelimit_bucket", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L27", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "target": "ratelimit_bucket", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L16", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "ratelimit_bucket", + "target": "internal_ratelimit_ratelimit_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L26", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "target": "internal_ratelimit_ratelimit_go_mutex", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "target": "ratelimit_limiter_allow", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L84", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "target": "ratelimit_limiter_cleanuploop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L105", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "target": "ratelimit_limiter_stop", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L36", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_new", + "target": "internal_ratelimit_ratelimit_go_ratelimit_limiter", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L43", + "weight": 1.0, + "_origin": "ast", + "source": "internal_ratelimit_ratelimit_new", + "target": "ratelimit_limiter_cleanuploop", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/ratelimit/ratelimit.go", + "source_location": "L86", + "weight": 1.0, + "_origin": "ast", + "source": "ratelimit_limiter_cleanuploop", + "target": "ratelimit_limiter_stop", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp", + "target": "internal_sieve_interp_evaltest", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L36", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp", + "target": "internal_sieve_interp_execstatements", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L29", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp", + "target": "internal_sieve_interp_execute", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L97", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp", + "target": "internal_sieve_interp_lookupheader", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L6", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp", + "target": "sieve_result", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L36", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_sieve_interp_execstatements", + "target": "sieve_result", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L29", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_sieve_interp_execute", + "target": "sieve_result", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L589", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_smtp_session_applysieve", + "target": "sieve_result", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp_execute", + "target": "internal_sieve_interp_execstatements", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L29", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_sieve_interp_execute", + "target": "sieve_script", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/smtp/session.go", + "source_location": "L595", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session_applysieve", + "target": "internal_sieve_interp_execute" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp_execstatements", + "target": "internal_sieve_interp_evaltest", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L36", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_sieve_interp_execstatements", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L83", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_interp_evaltest", + "target": "internal_sieve_interp_lookupheader", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/interp.go", + "source_location": "L78", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_sieve_interp_evaltest", + "target": "sieve_test", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_lexer", + "target": "internal_sieve_lexer_newlexer", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L35", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_lexer", + "target": "sieve_lexer", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_lexer", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L18", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_lexer", + "target": "sieve_tokenkind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L78", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "sieve_parser_expect", + "target": "sieve_tokenkind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L31", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_token", + "target": "sieve_tokenkind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L44", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_lexer_next", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L125", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_lexer_readident", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L99", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_lexer_readstring", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L116", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_lexer_readtag", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L49", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L78", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_parser_expect", + "target": "sieve_token", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L40", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_sieve_lexer_newlexer", + "target": "sieve_lexer", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L44", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer", + "target": "sieve_lexer_next", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L125", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer", + "target": "sieve_lexer_readident", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L99", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer", + "target": "sieve_lexer_readstring", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L116", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer", + "target": "sieve_lexer_readtag", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer", + "target": "sieve_lexer_skipwhitespaceandcomments", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L48", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_lexer", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/sieve/parser.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser_parse", + "target": "internal_sieve_lexer_newlexer" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L66", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer_next", + "target": "sieve_lexer_readident", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L62", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer_next", + "target": "sieve_lexer_readstring", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L64", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer_next", + "target": "sieve_lexer_readtag", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/lexer.go", + "source_location": "L45", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_lexer_next", + "target": "sieve_lexer_skipwhitespaceandcomments", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "internal_sieve_parser_parse", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_action", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_elseif", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L21", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_ifstatement", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L47", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_parser", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L7", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_script", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L12", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L38", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser", + "target": "sieve_test", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L52", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_sieve_parser_parse", + "target": "sieve_script", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L8", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_script", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L33", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_elseif", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L25", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_ifstatement", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L210", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_parser_parseblock", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L123", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_parser_parseif", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L89", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_parser_parsestatement", + "target": "sieve_statement", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L19", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_action", + "target": "sieve_action_isstatement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L24", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_ifstatement", + "target": "sieve_elseif", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L29", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_ifstatement", + "target": "sieve_ifstatement_isstatement", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L22", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_ifstatement", + "target": "sieve_test", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L32", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "sieve_elseif", + "target": "sieve_test", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L168", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "sieve_parser_parsetest", + "target": "sieve_test", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L69", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_parser_advance", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L78", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_parser_expect", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L210", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_parser_parseblock", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_parser_parseif", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_parser_parsestatement", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L168", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser", + "target": "sieve_parser_parsetest", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser_parse", + "target": "sieve_parser_advance", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L60", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_parser_parse", + "target": "sieve_parser_parsestatement", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/sieve/sieve_fuzz_test.go", + "source_location": "L30", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_sieve_fuzz_test_fuzzparse", + "target": "internal_sieve_parser_parse" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L83", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_expect", + "target": "sieve_parser_advance", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L124", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parseif", + "target": "sieve_parser_advance", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parsestatement", + "target": "sieve_parser_advance", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L174", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parsetest", + "target": "sieve_parser_advance", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L211", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parseblock", + "target": "sieve_parser_expect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L101", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parsestatement", + "target": "sieve_parser_expect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L198", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parsetest", + "target": "sieve_parser_expect", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L219", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parseblock", + "target": "sieve_parser_parsestatement", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L96", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parsestatement", + "target": "sieve_parser_parseif", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L131", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parseif", + "target": "sieve_parser_parseblock", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/parser.go", + "source_location": "L127", + "weight": 1.0, + "_origin": "ast", + "source": "sieve_parser_parseif", + "target": "sieve_parser_parsetest", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/sieve_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "_origin": "ast", + "source": "internal_sieve_sieve_fuzz_test", + "target": "internal_sieve_sieve_fuzz_test_fuzzparse", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/sieve/sieve_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_sieve_sieve_fuzz_test_fuzzparse", + "target": "internal_sieve_sieve_fuzz_test_go_f", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/auth.go", + "source_location": "L9", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_auth", + "target": "internal_smtp_auth_authenticate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/auth.go", + "source_location": "L9", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_auth_authenticate", + "target": "internal_smtp_auth_go_db", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/smtp/session.go", + "source_location": "L227", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handleauth", + "target": "internal_smtp_auth_authenticate" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L206", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_server", + "target": "internal_smtp_server_connhost", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L189", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_server", + "target": "internal_smtp_server_kindname", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_server", + "target": "internal_smtp_server_newserver", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_server", + "target": "smtp_kind", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L42", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_server", + "target": "smtp_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L189", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_server_kindname", + "target": "smtp_kind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L115", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_acceptloop", + "target": "smtp_kind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L143", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_handleconn", + "target": "smtp_kind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L35", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_kind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_smtp_server_newserver", + "target": "smtp_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L46", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "internal_smtp_server_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L44", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "internal_smtp_server_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L56", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "internal_smtp_server_go_limiter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L49", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "internal_smtp_server_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L51", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_server", + "target": "internal_smtp_server_go_waitgroup", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L115", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server", + "target": "smtp_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L182", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server", + "target": "smtp_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L143", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server", + "target": "smtp_server_handleconn", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L74", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server", + "target": "smtp_server_listenandserve", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L165", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server", + "target": "smtp_server_shutdown", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_server_newserver", + "target": "internal_smtp_server_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L59", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_server_newserver", + "target": "internal_smtp_server_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L115", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_acceptloop", + "target": "internal_smtp_server_go_listener", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L74", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_listenandserve", + "target": "internal_smtp_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_listenandserve", + "target": "internal_smtp_server_kindname", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L103", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_listenandserve", + "target": "smtp_server_acceptloop", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L89", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_listenandserve", + "target": "smtp_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L115", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_acceptloop", + "target": "internal_smtp_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L143", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_handleconn", + "target": "internal_smtp_server_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L128", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_acceptloop", + "target": "internal_smtp_server_connhost", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L123", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_acceptloop", + "target": "internal_smtp_server_kindname", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L138", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_acceptloop", + "target": "smtp_server_handleconn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L143", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_handleconn", + "target": "internal_smtp_server_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L158", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_handleconn", + "target": "internal_smtp_server_kindname", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L165", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_server_shutdown", + "target": "internal_smtp_server_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L166", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_server_shutdown", + "target": "smtp_server_closeall", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/server.go", + "source_location": "L206", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_server_connhost", + "target": "internal_smtp_server_go_addr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/smtp/session.go", + "source_location": "L200", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handleauth", + "target": "internal_smtp_server_connhost" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L589", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_applysieve", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L751", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_extractheader", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L601", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_extractheadermap", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L747", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_extractmessageid", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L743", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_extractsubject", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L624", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_injectspamheaders", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L708", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_parsemailcmdarg", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L736", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_senderipstring", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L693", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "internal_smtp_session_splitverb", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "smtp_recipienttarget", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L31", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "smtp_session", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L22", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session", + "target": "smtp_state", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L40", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_state", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L32", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "internal_smtp_session_go_conn", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L37", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "internal_smtp_session_go_ip", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L33", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "internal_smtp_session_go_readwriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L34", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "internal_smtp_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L45", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_recipienttarget", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L187", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handleauth", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handlecommand", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L379", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handledata", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L132", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handlehelo", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L285", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handlemailfrom", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L312", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handlercptto", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L164", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_handlestarttls", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_issubmissionkind", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L560", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_quarantinemessage", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L261", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_readauthlogin", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L239", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_readauthplain", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L668", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_readdotstuffed", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L658", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L639", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_reset", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_run", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L652", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L171", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlestarttls", + "target": "internal_smtp_session_go_server", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L736", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_smtp_session_senderipstring", + "target": "internal_smtp_session_go_ip", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L516", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "smtp_session_issubmissionkind", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L154", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlehelo", + "target": "smtp_session_issubmissionkind", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L286", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlemailfrom", + "target": "smtp_session_issubmissionkind", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L337", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlercptto", + "target": "smtp_session_issubmissionkind", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L58", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_session_run", + "target": "internal_smtp_session_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L84", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_run", + "target": "smtp_session_handlecommand", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L76", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_run", + "target": "smtp_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L65", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_run", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L92", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "internal_smtp_session_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L379", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "internal_smtp_session_go_context", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L98", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "internal_smtp_session_splitverb", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L108", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_handleauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L114", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_handledata", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L102", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_handlehelo", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L110", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_handlemailfrom", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L112", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_handlercptto", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_handlestarttls", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L116", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_reset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L94", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlecommand", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L137", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlehelo", + "target": "smtp_session_reset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L134", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlehelo", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L180", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlestarttls", + "target": "smtp_session_reset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L166", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlestarttls", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L217", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handleauth", + "target": "smtp_session_readauthlogin", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L215", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handleauth", + "target": "smtp_session_readauthplain", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L189", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handleauth", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L243", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_readauthplain", + "target": "smtp_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L242", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_readauthplain", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L263", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_readauthlogin", + "target": "smtp_session_readline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L262", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_readauthlogin", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L291", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlemailfrom", + "target": "internal_smtp_session_parsemailcmdarg", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L287", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlemailfrom", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L322", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlercptto", + "target": "internal_smtp_session_parsemailcmdarg", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L314", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handlercptto", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L484", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "internal_smtp_session_applysieve", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L400", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "internal_smtp_session_extractmessageid", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L399", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "internal_smtp_session_extractsubject", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L462", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "internal_smtp_session_injectspamheaders", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L415", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "internal_smtp_session_senderipstring", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L468", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "smtp_session_quarantinemessage", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L388", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "smtp_session_readdotstuffed", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L395", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "smtp_session_reset", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L381", + "weight": 1.0, + "_origin": "ast", + "source": "smtp_session_handledata", + "target": "smtp_session_writeline", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L594", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session_applysieve", + "target": "internal_smtp_session_extractheadermap", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L744", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session_extractsubject", + "target": "internal_smtp_session_extractheader", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/smtp/session.go", + "source_location": "L748", + "weight": 1.0, + "_origin": "ast", + "source": "internal_smtp_session_extractmessageid", + "target": "internal_smtp_session_extractheader", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L37", + "weight": 1.0, + "_origin": "ast", + "source": "internal_tlsutil_acme_manager", + "target": "internal_tlsutil_acme_manager_newacmemanager", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_tlsutil_acme_manager", + "target": "tlsutil_acmemanager", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L37", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_tlsutil_acme_manager_newacmemanager", + "target": "tlsutil_acmemanager", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L34", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "internal_tlsutil_acme_manager_go_certificate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L27", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "internal_tlsutil_acme_manager_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L33", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "internal_tlsutil_acme_manager_go_rwmutex", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L58", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "tlsutil_acmemanager_certificatefor", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L80", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "tlsutil_acmemanager_loadfromdb", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L160", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "tlsutil_acmemanager_loadorcreateaccountkey", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L108", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "tlsutil_acmemanager_obtainandstore", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L198", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "tlsutil_acmemanager_startrenewalloop", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager", + "target": "tlsutil_acmemanager_tlsconfig", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L37", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_tlsutil_acme_manager_newacmemanager", + "target": "internal_tlsutil_acme_manager_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L58", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_certificatefor", + "target": "internal_tlsutil_acme_manager_go_certificate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L80", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_loadfromdb", + "target": "internal_tlsutil_acme_manager_go_certificate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L108", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_obtainandstore", + "target": "internal_tlsutil_acme_manager_go_certificate", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L46", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_tlsconfig", + "target": "internal_tlsutil_acme_manager_go_config", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L50", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_tlsconfig", + "target": "tlsutil_acmemanager_certificatefor", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L66", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_certificatefor", + "target": "tlsutil_acmemanager_loadfromdb", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L73", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_certificatefor", + "target": "tlsutil_acmemanager_obtainandstore", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L109", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_obtainandstore", + "target": "tlsutil_acmemanager_loadorcreateaccountkey", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L213", + "weight": 1.0, + "_origin": "ast", + "source": "tlsutil_acmemanager_startrenewalloop", + "target": "tlsutil_acmemanager_obtainandstore", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L198", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_startrenewalloop", + "target": "internal_tlsutil_acme_manager_go_context", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/acme_manager.go", + "source_location": "L198", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "tlsutil_acmemanager_startrenewalloop", + "target": "internal_tlsutil_acme_manager_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L77", + "weight": 1.0, + "_origin": "ast", + "source": "internal_tlsutil_selfsigned", + "target": "internal_tlsutil_selfsigned_generateselfsigned", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "internal_tlsutil_selfsigned", + "target": "internal_tlsutil_selfsigned_loadorgenerate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L70", + "weight": 1.0, + "_origin": "ast", + "source": "internal_tlsutil_selfsigned", + "target": "internal_tlsutil_selfsigned_parseminversion", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "internal_tlsutil_selfsigned_loadorgenerate", + "target": "internal_tlsutil_selfsigned_generateselfsigned", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L27", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_tlsutil_selfsigned_loadorgenerate", + "target": "internal_tlsutil_selfsigned_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/tlsutil/selfsigned.go", + "source_location": "L77", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_tlsutil_selfsigned_generateselfsigned", + "target": "internal_tlsutil_selfsigned_go_certificate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L92", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp", + "target": "internal_totp_totp_decodesecret", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L40", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp", + "target": "internal_totp_totp_generate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L29", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp", + "target": "internal_totp_totp_generatesecret", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L71", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp", + "target": "internal_totp_totp_hotp", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L114", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp", + "target": "internal_totp_totp_provisioninguri", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L52", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp", + "target": "internal_totp_totp_validate", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L664", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_totp_totp_generatesecret" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp_generate", + "target": "internal_totp_totp_decodesecret", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L40", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_totp_totp_generate", + "target": "internal_totp_totp_go_time", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L46", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp_generate", + "target": "internal_totp_totp_hotp", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L53", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp_validate", + "target": "internal_totp_totp_decodesecret", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/totp/totp.go", + "source_location": "L62", + "weight": 1.0, + "_origin": "ast", + "source": "internal_totp_totp_validate", + "target": "internal_totp_totp_hotp", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L706", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_totp_totp_validate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L178", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_totp_totp_validate" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L679", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_totp_totp_provisioninguri" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L134", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard", + "target": "internal_vcard_vcard_escape", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L25", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard", + "target": "internal_vcard_vcard_parse", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L106", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard", + "target": "internal_vcard_vcard_splitproperty", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L142", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard", + "target": "internal_vcard_vcard_unescape", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L121", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard", + "target": "internal_vcard_vcard_unfold", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L14", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard", + "target": "vcard_card", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L25", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_vcard_vcard_parse", + "target": "vcard_card", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L77", + "weight": 1.0, + "_origin": "ast", + "source": "vcard_card", + "target": "vcard_card_build", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/vcard/vcard_fuzz_test.go", + "source_location": "L27", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard_fuzz_test_fuzzparse", + "target": "internal_vcard_vcard_parse" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard_parse", + "target": "internal_vcard_vcard_splitproperty", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L66", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard_parse", + "target": "internal_vcard_vcard_unescape", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L26", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard_parse", + "target": "internal_vcard_vcard_unfold", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard.go", + "source_location": "L83", + "weight": 1.0, + "_origin": "ast", + "source": "vcard_card_build", + "target": "internal_vcard_vcard_escape", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "_origin": "ast", + "source": "internal_vcard_vcard_fuzz_test", + "target": "internal_vcard_vcard_fuzz_test_fuzzparse", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/vcard/vcard_fuzz_test.go", + "source_location": "L5", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_vcard_vcard_fuzz_test_fuzzparse", + "target": "internal_vcard_vcard_fuzz_test_go_f", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "internal_webmail_api_newhandler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L850", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "internal_webmail_api_parseduration", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L641", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "internal_webmail_api_randomstate", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L651", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "internal_webmail_api_sha256hex", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L94", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L88", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L100", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "webmail_ctxkey", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L36", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "webmail_handler", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L48", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api", + "target": "webmail_oauthstateentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_webmail_api_newhandler", + "target": "webmail_handler", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L42", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_handler", + "target": "internal_webmail_api_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L37", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_handler", + "target": "internal_webmail_api_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L44", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_handler", + "target": "internal_webmail_api_go_mutex", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L832", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_apppasswordbyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L759", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_apppasswords", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L498", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_deleteaccount", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L867", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_forgotpassword", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L244", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_getme", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L472", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_listaccounts", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L256", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_listfolders", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L266", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_listmessages", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L381", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_listquarantine", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L104", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_login", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L320", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_messagebyid", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L685", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_mfaconfirm", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L733", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_mfadisable", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L659", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_mfasetup", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L151", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_mfaverify", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L566", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_oauthcallback", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L524", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_oauthdispatch", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L545", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_oauthstart", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L252", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_provider", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L632", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_pruneexpiredstate", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L62", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_registerroutes", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L390", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_releasequarantine", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L894", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_resetpassword", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L291", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_sendorlistmessages", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L921", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_setrecoveryemail", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L437", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_sseevents", + "confidence_score": 1.0 + }, + { + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L208", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_handler_withauth", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L45", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_handler", + "target": "webmail_oauthstateentry", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webmail_api_newhandler", + "target": "internal_webmail_api_go_db", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L54", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webmail_api_newhandler", + "target": "internal_webmail_api_go_config", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L51", + "weight": 1.0, + "context": "field", + "_origin": "ast", + "source": "webmail_oauthstateentry", + "target": "internal_webmail_api_go_time", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L62", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_registerroutes", + "target": "internal_webmail_api_go_servemux", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L67", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_registerroutes", + "target": "webmail_handler_withauth", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L95", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api_writeerr", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L88", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webmail_api_writejson", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L847", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_apppasswordbyid", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L781", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L517", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_deleteaccount", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L891", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_forgotpassword", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L245", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_getme", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L495", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listaccounts", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L262", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listfolders", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L288", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listmessages", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L387", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listquarantine", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L131", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L341", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_messagebyid", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L730", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L754", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfadisable", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L680", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L202", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L629", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthcallback", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L563", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L427", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_releasequarantine", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L918", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_resetpassword", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L316", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_sendorlistmessages", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L935", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_setrecoveryemail", + "target": "internal_webmail_api_writejson", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L94", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webmail_api_writeerr", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L832", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_apppasswordbyid", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L759", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L498", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_deleteaccount", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L867", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_forgotpassword", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L244", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_getme", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L472", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listaccounts", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L256", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listfolders", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L266", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listmessages", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L381", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listquarantine", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L104", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L320", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_messagebyid", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L685", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L733", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfadisable", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L659", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L151", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L566", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthcallback", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L524", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthdispatch", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L545", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L390", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_releasequarantine", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L894", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_resetpassword", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L291", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_sendorlistmessages", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L921", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_setrecoveryemail", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L437", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_sseevents", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L208", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_withauth", + "target": "internal_webmail_api_go_responsewriter", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L840", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_apppasswordbyid", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L764", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L510", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_deleteaccount", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L475", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listaccounts", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L259", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listfolders", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L285", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listmessages", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L384", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listquarantine", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L111", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L338", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_messagebyid", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L692", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L740", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfadisable", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L666", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L158", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L570", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthcallback", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L548", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L399", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_releasequarantine", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L901", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_resetpassword", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L303", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_sendorlistmessages", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L928", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_setrecoveryemail", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L440", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_sseevents", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L217", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_withauth", + "target": "internal_webmail_api_writeerr", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L104", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L135", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_webtoken_webtoken_issue" + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L126", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_login", + "target": "internal_webtoken_webtoken_issuewithpurpose" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L832", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_apppasswordbyid", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L759", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L498", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_deleteaccount", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L867", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_forgotpassword", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L244", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_getme", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L472", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listaccounts", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L256", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listfolders", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L266", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listmessages", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L381", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_listquarantine", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L320", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_messagebyid", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L685", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L733", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfadisable", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L659", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfasetup", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L151", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L566", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthcallback", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L524", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthdispatch", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L545", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L390", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_releasequarantine", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L894", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_resetpassword", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L291", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_sendorlistmessages", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L921", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_setrecoveryemail", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L437", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_sseevents", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L208", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "webmail_handler_withauth", + "target": "internal_webmail_api_go_request", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L185", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_webmail_api_sha256hex", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L195", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaverify", + "target": "internal_webtoken_webtoken_issue" + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L535", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthdispatch", + "target": "webmail_handler_withauth", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L208", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "webmail_handler_withauth", + "target": "internal_webmail_api_go_handlerfunc", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L257", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listfolders", + "target": "webmail_handler_provider", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L283", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_listmessages", + "target": "webmail_handler_provider", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L332", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_messagebyid", + "target": "webmail_handler_provider", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L312", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_sendorlistmessages", + "target": "webmail_handler_provider", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L539", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthdispatch", + "target": "webmail_handler_oauthcallback", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L536", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthdispatch", + "target": "webmail_handler_oauthstart", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L552", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "internal_webmail_api_randomstate", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L559", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_oauthstart", + "target": "webmail_handler_pruneexpiredstate", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L719", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_mfaconfirm", + "target": "internal_webmail_api_sha256hex", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L808", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_apppasswords", + "target": "internal_webmail_api_parseduration", + "confidence_score": 1.0 + }, + { + "relation": "references", + "context": "return_type", + "confidence": "EXTRACTED", + "source_file": "internal/webmail/api.go", + "source_location": "L850", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webmail_api_parseduration", + "target": "internal_webmail_api_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "internal/webmail/api.go", + "source_location": "L877", + "weight": 1.0, + "_origin": "ast", + "source": "webmail_handler_forgotpassword", + "target": "internal_webtoken_webtoken_issuewithpurpose" + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L97", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "internal_webtoken_webtoken_base64urldecode", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L93", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "internal_webtoken_webtoken_base64urlencode", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L33", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "internal_webtoken_webtoken_issue", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L41", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "internal_webtoken_webtoken_issuewithpurpose", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L87", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "internal_webtoken_webtoken_sign", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L59", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "internal_webtoken_webtoken_verify", + "confidence_score": 1.0 + }, + { + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L21", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken", + "target": "webtoken_claims", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L59", + "weight": 1.0, + "context": "return_type", + "_origin": "ast", + "source": "internal_webtoken_webtoken_verify", + "target": "webtoken_claims", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L33", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webtoken_webtoken_issue", + "target": "internal_webtoken_webtoken_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L34", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken_issue", + "target": "internal_webtoken_webtoken_issuewithpurpose", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L41", + "weight": 1.0, + "context": "parameter_type", + "_origin": "ast", + "source": "internal_webtoken_webtoken_issuewithpurpose", + "target": "internal_webtoken_webtoken_go_duration", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L51", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken_issuewithpurpose", + "target": "internal_webtoken_webtoken_base64urlencode", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L54", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken_issuewithpurpose", + "target": "internal_webtoken_webtoken_sign", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L72", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken_verify", + "target": "internal_webtoken_webtoken_base64urldecode", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L65", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken_verify", + "target": "internal_webtoken_webtoken_sign", + "confidence_score": 1.0 + }, + { + "relation": "calls", + "context": "call", + "confidence": "EXTRACTED", + "source_file": "internal/webtoken/webtoken.go", + "source_location": "L90", + "weight": 1.0, + "_origin": "ast", + "source": "internal_webtoken_webtoken_sign", + "target": "internal_webtoken_webtoken_base64urlencode", + "confidence_score": 1.0 + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps.skill", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_package" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_api_helper" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_cache_busting" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_dark_theme" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_js_scoping_bug" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_no_deps_principle" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_renderer" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_go_web_app_no_deps_skill_security_basics" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_pattern", + "target": "_claude_iterative_build_discipline_skill_gomail_project" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_block_bleed_bug", + "target": "_claude_go_web_app_no_deps_skill_renderer" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_renderer", + "target": "_claude_go_web_app_no_deps_skill_base_html" + }, + { + "relation": "conceptually_related_to", + "confidence": "AMBIGUOUS", + "confidence_score": 0.2, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_renderer", + "target": "internal_admin_static_index_page" + }, + { + "relation": "conceptually_related_to", + "confidence": "AMBIGUOUS", + "confidence_score": 0.2, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_base_html", + "target": "internal_webmail_static_index_page" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.75, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_dark_theme", + "target": "internal_admin_static_index_page" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.75, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_dark_theme", + "target": "internal_webmail_static_index_page" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_api_helper", + "target": "internal_admin_static_index_api" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_api_helper", + "target": "internal_webmail_static_index_api" + }, + { + "relation": "conceptually_related_to", + "confidence": "AMBIGUOUS", + "confidence_score": 0.2, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_security_basics", + "target": "internal_admin_static_index_page" + }, + { + "relation": "conceptually_related_to", + "confidence": "AMBIGUOUS", + "confidence_score": 0.2, + "source_file": ".claude/go-web-app-no-deps-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_go_web_app_no_deps_skill_security_basics", + "target": "internal_webmail_static_index_page" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_no_third_party_rule", + "target": "_claude_go_web_app_no_deps_skill_no_deps_principle" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline.skill", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_core_principle", + "target": "_claude_iterative_build_discipline_package" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_core_principle", + "target": "_claude_iterative_build_discipline_skill_e2e_testing" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_core_principle", + "target": "_claude_iterative_build_discipline_skill_gomail_project" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_e2e_testing", + "target": "_claude_iterative_build_discipline_skill_fake_protocol_server" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_e2e_testing", + "target": "_claude_iterative_build_discipline_skill_negative_tests" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_fake_protocol_server", + "target": "_claude_iterative_build_discipline_skill_test_vectors" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_checkpoint_artifact", + "target": "_claude_iterative_build_discipline_skill_living_docs" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_scope_honesty", + "target": "_claude_iterative_build_discipline_skill_risk_prioritization" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_tcp_read_pattern", + "target": "_claude_iterative_build_discipline_skill_gomail_project" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_tcp_read_pattern", + "target": "gomail_action_plan_v4_tcp_bug" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_tcp_read_pattern", + "target": "gomail_handover_tcp_bug" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_sqlite_single_conn", + "target": "_claude_iterative_build_discipline_skill_gomail_project" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_sqlite_single_conn", + "target": "gomail_action_plan_v4_sqlite_deadlock_bug" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_sqlite_single_conn", + "target": "gomail_handover_sqlite_deadlock_bug" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_wire_struct_tags", + "target": "_claude_iterative_build_discipline_skill_gomail_project" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_gomail_project", + "target": "gomail_action_plan_v4_overview" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_gomail_project", + "target": "gomail_handover_overview" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": ".claude/iterative-build-discipline-SKILL.md", + "source_location": null, + "weight": 1.0, + "source": "_claude_iterative_build_discipline_skill_gomail_project", + "target": "readme_gomail" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_overview", + "target": "gomail_action_plan_v4_overview" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_sandbox_constraints", + "target": "gomail_handover_setup_steps" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_setup_steps", + "target": "readme_dns_setup" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_setup_steps", + "target": "readme_gomail" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_decisions_not_to_relitigate", + "target": "gomail_action_plan_v4_phase10" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_decisions_not_to_relitigate", + "target": "gomail_action_plan_v4_phase5" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_decisions_not_to_relitigate", + "target": "gomail_action_plan_v4_phase7" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_decisions_not_to_relitigate", + "target": "gomail_action_plan_v4_phase8" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_decisions_not_to_relitigate", + "target": "gomail_action_plan_v4_phase9" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_decisions_not_to_relitigate", + "target": "internal_webmail_static_index_page" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_next_session_scope", + "target": "gomail_action_plan_v4_phase10" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_next_session_scope", + "target": "gomail_action_plan_v4_phase11" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_next_session_scope", + "target": "gomail_action_plan_v4_phase12" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_next_session_scope", + "target": "gomail_action_plan_v4_phase13" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_next_session_scope", + "target": "gomail_action_plan_v4_phase14" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_next_session_scope", + "target": "gomail_action_plan_v4_phase15" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_tcp_bug", + "target": "gomail_action_plan_v4_tcp_bug" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "GOMAIL_HANDOVER.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_handover_sqlite_deadlock_bug", + "target": "gomail_action_plan_v4_sqlite_deadlock_bug" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "README.md", + "source_location": null, + "weight": 1.0, + "source": "readme_gomail", + "target": "gomail_action_plan_v4_overview" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "README.md", + "source_location": null, + "weight": 1.0, + "source": "readme_gomail", + "target": "readme_dns_setup" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "README.md", + "source_location": null, + "weight": 1.0, + "source": "readme_gomail", + "target": "readme_tls_acme" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "README.md", + "source_location": null, + "weight": 1.0, + "source": "readme_tls_acme", + "target": "gomail_action_plan_v4_phase13" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.75, + "source_file": "README.md", + "source_location": null, + "weight": 1.0, + "source": "readme_dns_setup", + "target": "gomail_action_plan_v4_phase11" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "README.md", + "source_location": null, + "weight": 1.0, + "source": "readme_dns_setup", + "target": "gomail_action_plan_v4_phase13" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_e2etest_pattern" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase10" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase11" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase12" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase13" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase14" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase15" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase5" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase7" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase8" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase9" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_phase9_5" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_overview", + "target": "gomail_action_plan_v4_sandbox_constraints" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase8", + "target": "internal_webmail_static_index_compose" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase8", + "target": "internal_webmail_static_index_folders" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase8", + "target": "internal_webmail_static_index_page" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase8", + "target": "internal_webmail_static_index_quarantine" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_admin_domain_tenant_bug", + "target": "gomail_action_plan_v4_phase11" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_admin_user_domainid_bug", + "target": "gomail_action_plan_v4_phase11" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase11", + "target": "internal_admin_static_index_domains_crud" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase11", + "target": "internal_admin_static_index_quarantine" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase11", + "target": "internal_admin_static_index_queue_crud" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase11", + "target": "internal_admin_static_index_rules_crud" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase11", + "target": "internal_admin_static_index_users_crud" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_quarantine_admin_vs_webmail", + "target": "gomail_action_plan_v4_phase11" + }, + { + "relation": "references", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase13_pulled_forward_rationale", + "target": "gomail_action_plan_v4_phase12" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_sqlite_deadlock_bug", + "target": "gomail_action_plan_v4_phase12" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_phase13_pulled_forward_rationale", + "target": "gomail_action_plan_v4_phase13" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_eicar_verification", + "target": "gomail_action_plan_v4_phase14" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_tcp_bug", + "target": "gomail_action_plan_v4_phase14" + }, + { + "relation": "rationale_for", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_rate_limiter", + "target": "gomail_action_plan_v4_phase15" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_quarantine_admin_vs_webmail", + "target": "internal_admin_static_index_quarantine" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_quarantine_admin_vs_webmail", + "target": "internal_webmail_static_index_quarantine" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_admin_user_domainid_bug", + "target": "internal_admin_static_index_domains_crud" + }, + { + "relation": "references", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": "gomail-action-plan-v4.md", + "source_location": null, + "weight": 1.0, + "source": "gomail_action_plan_v4_admin_user_domainid_bug", + "target": "internal_admin_static_index_users_crud" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": "internal/admin/static/index.html", + "source_location": null, + "weight": 1.0, + "source": "internal_admin_static_index_api", + "target": "internal_webmail_static_index_api" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "internal/admin/static/index.html", + "source_location": null, + "weight": 1.0, + "source": "internal_admin_static_index_login", + "target": "internal_webmail_static_index_login" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.75, + "source_file": "internal/admin/static/index.html", + "source_location": null, + "weight": 1.0, + "source": "internal_admin_static_index_showapp", + "target": "internal_webmail_static_index_showapp" + }, + { + "relation": "conceptually_related_to", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "internal/admin/static/index.html", + "source_location": null, + "weight": 1.0, + "source": "internal_admin_static_index_quarantine", + "target": "internal_webmail_static_index_quarantine" + }, + { + "relation": "semantically_similar_to", + "confidence": "INFERRED", + "confidence_score": 0.95, + "source_file": "internal/admin/static/index.html", + "source_location": null, + "weight": 1.0, + "source": "internal_admin_static_index_esc", + "target": "internal_webmail_static_index_esc" + } + ], + "hyperedges": [ + { + "id": "gomail_project_documentation_set", + "label": "GoMail Project Documentation Set", + "nodes": [ + "gomail_handover_overview", + "readme_gomail", + "gomail_action_plan_v4_overview", + "_claude_iterative_build_discipline_skill_gomail_project" + ], + "relation": "participate_in", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "GOMAIL_HANDOVER.md" + }, + { + "id": "admin_portal_crud_feature_set", + "label": "Admin Portal CRUD Feature Set", + "nodes": [ + "internal_admin_static_index_domains_crud", + "internal_admin_static_index_users_crud", + "internal_admin_static_index_rules_crud", + "internal_admin_static_index_queue_crud", + "internal_admin_static_index_quarantine" + ], + "relation": "implement", + "confidence": "EXTRACTED", + "confidence_score": 1.0, + "source_file": "internal/admin/static/index.html" + }, + { + "id": "recurring_bug_pattern_docs", + "label": "Recurring Bug-Pattern Documentation Across GoMail Docs", + "nodes": [ + "_claude_iterative_build_discipline_skill_sqlite_single_conn", + "gomail_handover_sqlite_deadlock_bug", + "gomail_action_plan_v4_sqlite_deadlock_bug", + "_claude_iterative_build_discipline_skill_tcp_read_pattern", + "gomail_handover_tcp_bug", + "gomail_action_plan_v4_tcp_bug" + ], + "relation": "form", + "confidence": "INFERRED", + "confidence_score": 0.85, + "source_file": "GOMAIL_HANDOVER.md" + } + ] +} \ No newline at end of file diff --git a/graphify-out/manifest.json b/graphify-out/manifest.json new file mode 100644 index 0000000..6dae038 --- /dev/null +++ b/graphify-out/manifest.json @@ -0,0 +1,392 @@ +{ + ".claude/settings.json": { + "mtime": 1786281487.088728, + "ast_hash": "22d1c817e36c7eccf36bf30ee13299cc", + "semantic_hash": "22d1c817e36c7eccf36bf30ee13299cc" + }, + "cmd/gomail/main.go": { + "mtime": 1786281911.6889164, + "ast_hash": "6a50dc1a9b0161c4c030b51307ff92cf", + "semantic_hash": "6a50dc1a9b0161c4c030b51307ff92cf" + }, + "go.mod": { + "mtime": 1786281935.7638168, + "ast_hash": "38e6ab128168a0a88095fec4523f2992", + "semantic_hash": "38e6ab128168a0a88095fec4523f2992" + }, + "internal/accounts/link.go": { + "mtime": 1786281887.2038412, + "ast_hash": "4ffb8f5d3a6ac987d960c702f00f4e07", + "semantic_hash": "4ffb8f5d3a6ac987d960c702f00f4e07" + }, + "internal/accounts/provider.go": { + "mtime": 1786168320.0, + "ast_hash": "4a180151e798601f43bda25bd1a80410", + "semantic_hash": "4a180151e798601f43bda25bd1a80410" + }, + "internal/accounts/provider_gomail.go": { + "mtime": 1786281891.3509514, + "ast_hash": "f0dd245cb144dca3f03520e632c1d5f5", + "semantic_hash": "f0dd245cb144dca3f03520e632c1d5f5" + }, + "internal/accounts/provider_imap.go": { + "mtime": 1786281893.9837677, + "ast_hash": "99587258525acd5ab4b20398a2d0d400", + "semantic_hash": "99587258525acd5ab4b20398a2d0d400" + }, + "internal/accounts/provider_smtp_helper.go": { + "mtime": 1786281896.5890837, + "ast_hash": "188d01aeb8fe3fb73cb76a25ffda71b4", + "semantic_hash": "188d01aeb8fe3fb73cb76a25ffda71b4" + }, + "internal/acme/challenge.go": { + "mtime": 1786189310.0, + "ast_hash": "ec3bcb998b34bc2c8a43cb8f3880e112", + "semantic_hash": "ec3bcb998b34bc2c8a43cb8f3880e112" + }, + "internal/acme/client.go": { + "mtime": 1786189290.0, + "ast_hash": "830b78389d159a75e48f1a0076b6275b", + "semantic_hash": "830b78389d159a75e48f1a0076b6275b" + }, + "internal/acme/jws.go": { + "mtime": 1786189220.0, + "ast_hash": "e5aa7cbbea33d9971f1577e4c976cb8c", + "semantic_hash": "e5aa7cbbea33d9971f1577e4c976cb8c" + }, + "internal/acme/obtain.go": { + "mtime": 1786189332.0, + "ast_hash": "d2f78794831ad67ba64fb79cb7eb1c00", + "semantic_hash": "d2f78794831ad67ba64fb79cb7eb1c00" + }, + "internal/admin/api.go": { + "mtime": 1786281901.2009063, + "ast_hash": "14fbb199cccd839edb6cf98392226b03", + "semantic_hash": "14fbb199cccd839edb6cf98392226b03" + }, + "internal/admin/embed.go": { + "mtime": 1786188384.0, + "ast_hash": "55b64461208f9449c417bddf49e686cc", + "semantic_hash": "55b64461208f9449c417bddf49e686cc" + }, + "internal/admin/handlers.go": { + "mtime": 1786281901.8545587, + "ast_hash": "06aad8456678b41905ac6787e50fb15a", + "semantic_hash": "06aad8456678b41905ac6787e50fb15a" + }, + "internal/auth/auth.go": { + "mtime": 1786281903.3781548, + "ast_hash": "c74ed7b257b2272dcfb860b8a60746a0", + "semantic_hash": "c74ed7b257b2272dcfb860b8a60746a0" + }, + "internal/config/config.go": { + "mtime": 1786189702.0, + "ast_hash": "12cee16f6dcfa6971217dd104e86f4d6", + "semantic_hash": "12cee16f6dcfa6971217dd104e86f4d6" + }, + "internal/crypto/crypto.go": { + "mtime": 1784360971.0, + "ast_hash": "4567100200969235fd1559ad9be6f1b1", + "semantic_hash": "4567100200969235fd1559ad9be6f1b1" + }, + "internal/dav/dav.go": { + "mtime": 1786281900.5555487, + "ast_hash": "9dc713615ff99b713ad2879fca2b28dd", + "semantic_hash": "9dc713615ff99b713ad2879fca2b28dd" + }, + "internal/db/bootstrap.go": { + "mtime": 1786281902.3275623, + "ast_hash": "922a08de72be0ef63409ed402d01e098", + "semantic_hash": "922a08de72be0ef63409ed402d01e098" + }, + "internal/db/db.go": { + "mtime": 1784361005.0, + "ast_hash": "64d527aad454dab7867ffd51aa3d72ca", + "semantic_hash": "64d527aad454dab7867ffd51aa3d72ca" + }, + "internal/db/migrations.go": { + "mtime": 1786193366.0, + "ast_hash": "d67ddd29ab706ae5ce670da624e30b2c", + "semantic_hash": "d67ddd29ab706ae5ce670da624e30b2c" + }, + "internal/db/models.go": { + "mtime": 1786193384.0, + "ast_hash": "cf35e391db47a84bd0b4e05ba77b9a95", + "semantic_hash": "cf35e391db47a84bd0b4e05ba77b9a95" + }, + "internal/db/queries.go": { + "mtime": 1786193758.0, + "ast_hash": "c5f7a40030988031163220e54051b93d", + "semantic_hash": "c5f7a40030988031163220e54051b93d" + }, + "internal/dkim/keys.go": { + "mtime": 1785566243.0, + "ast_hash": "e8d336df9cba3a8f69c70962f8a75579", + "semantic_hash": "e8d336df9cba3a8f69c70962f8a75579" + }, + "internal/dkim/sign.go": { + "mtime": 1785472923.0, + "ast_hash": "ba6dd425f253aac61b386a8aea0d2061", + "semantic_hash": "ba6dd425f253aac61b386a8aea0d2061" + }, + "internal/dkim/verify.go": { + "mtime": 1785472944.0, + "ast_hash": "4e58aca20a4874dac789bc0a844bfba6", + "semantic_hash": "4e58aca20a4874dac789bc0a844bfba6" + }, + "internal/ical/ical.go": { + "mtime": 1785905665.0, + "ast_hash": "2de79f76c377508c20caa2e8683e920a", + "semantic_hash": "2de79f76c377508c20caa2e8683e920a" + }, + "internal/ical/ical_fuzz_test.go": { + "mtime": 1786207598.0, + "ast_hash": "5518fd86059d729048685e2bc39c24b2", + "semantic_hash": "5518fd86059d729048685e2bc39c24b2" + }, + "internal/imap/commands.go": { + "mtime": 1786281888.6448092, + "ast_hash": "e0a26bce9488a5d0bc680d5b11d15610", + "semantic_hash": "e0a26bce9488a5d0bc680d5b11d15610" + }, + "internal/imap/parser.go": { + "mtime": 1785638411.0, + "ast_hash": "687b7788b077688b9081f958b669a0e6", + "semantic_hash": "687b7788b077688b9081f958b669a0e6" + }, + "internal/imap/server.go": { + "mtime": 1786281911.0781865, + "ast_hash": "13154da7fca4635077e377476aef64c8", + "semantic_hash": "13154da7fca4635077e377476aef64c8" + }, + "internal/imap/session.go": { + "mtime": 1786281910.5246246, + "ast_hash": "f06bd7a253f498fbe197951537bc589e", + "semantic_hash": "f06bd7a253f498fbe197951537bc589e" + }, + "internal/imap/tokenize_fuzz_test.go": { + "mtime": 1786207631.0, + "ast_hash": "4920db9f0c2beaa06a7393ab027d3c2b", + "semantic_hash": "4920db9f0c2beaa06a7393ab027d3c2b" + }, + "internal/imapclient/client.go": { + "mtime": 1786169633.0, + "ast_hash": "f9671c971ff9f39a2ed82af7ed3e87df", + "semantic_hash": "f9671c971ff9f39a2ed82af7ed3e87df" + }, + "internal/jmap/jmap.go": { + "mtime": 1786281904.3499777, + "ast_hash": "2484a0bccd56908627cee4c769b689e5", + "semantic_hash": "2484a0bccd56908627cee4c769b689e5" + }, + "internal/mailstore/maildir.go": { + "mtime": 1786281898.3165317, + "ast_hash": "7224a5c029cdcdc63c0de93cb8a8eb51", + "semantic_hash": "7224a5c029cdcdc63c0de93cb8a8eb51" + }, + "internal/managesieve/server.go": { + "mtime": 1786281899.3706038, + "ast_hash": "4ebc166b83227affe5171026b8012876", + "semantic_hash": "4ebc166b83227affe5171026b8012876" + }, + "internal/managesieve/session.go": { + "mtime": 1786281899.9771776, + "ast_hash": "ab69e22b6add70fdb6c18f056c474e8d", + "semantic_hash": "ab69e22b6add70fdb6c18f056c474e8d" + }, + "internal/oauth2/oauth2.go": { + "mtime": 1786169607.0, + "ast_hash": "5ab8fd85a331f7f82470ee354e309ec4", + "semantic_hash": "5ab8fd85a331f7f82470ee354e309ec4" + }, + "internal/pipeline/pipeline.go": { + "mtime": 1786281906.8561504, + "ast_hash": "2b0b026fc13ce700c083960df7bc7857", + "semantic_hash": "2b0b026fc13ce700c083960df7bc7857" + }, + "internal/pipeline/stage_clamav.go": { + "mtime": 1786281905.5831316, + "ast_hash": "6a5c073d349a49e2cbe75863945a663b", + "semantic_hash": "6a5c073d349a49e2cbe75863945a663b" + }, + "internal/pipeline/stage_dkim.go": { + "mtime": 1786281908.573951, + "ast_hash": "3f92da867d5b128398a8ffc70b6fa475", + "semantic_hash": "3f92da867d5b128398a8ffc70b6fa475" + }, + "internal/pipeline/stage_dmarc.go": { + "mtime": 1786281917.6531692, + "ast_hash": "2d73846ca47178c53a0ffb341b7d754e", + "semantic_hash": "2d73846ca47178c53a0ffb341b7d754e" + }, + "internal/pipeline/stage_headers.go": { + "mtime": 1786281909.176789, + "ast_hash": "e7777ff0afde0baa97bd458060e7c002", + "semantic_hash": "e7777ff0afde0baa97bd458060e7c002" + }, + "internal/pipeline/stage_llm.go": { + "mtime": 1786281906.2809672, + "ast_hash": "c0a1d11888d0f32083b0e90d32018ec1", + "semantic_hash": "c0a1d11888d0f32083b0e90d32018ec1" + }, + "internal/pipeline/stage_rspamd.go": { + "mtime": 1786281907.8549173, + "ast_hash": "6b7c65df77472ef32c96782db5d98d07", + "semantic_hash": "6b7c65df77472ef32c96782db5d98d07" + }, + "internal/pipeline/stage_spf.go": { + "mtime": 1786281916.382805, + "ast_hash": "7e1eda77e5ba190bc50dda977b0c3723", + "semantic_hash": "7e1eda77e5ba190bc50dda977b0c3723" + }, + "internal/pipeline/stage_url.go": { + "mtime": 1786281905.1305835, + "ast_hash": "f954d508ac085f560928c06171779db5", + "semantic_hash": "f954d508ac085f560928c06171779db5" + }, + "internal/pop3/pop3.go": { + "mtime": 1786281909.7660527, + "ast_hash": "0e3685f20c9ca05d3a28729c8ad7a29f", + "semantic_hash": "0e3685f20c9ca05d3a28729c8ad7a29f" + }, + "internal/queue/queue.go": { + "mtime": 1786281808.0265675, + "ast_hash": "71c0ce0211d61d37f51f19678dbcdeb4", + "semantic_hash": "71c0ce0211d61d37f51f19678dbcdeb4" + }, + "internal/ratelimit/http.go": { + "mtime": 1786207203.0, + "ast_hash": "c06b19193ad3c3159551966c05f48ea5", + "semantic_hash": "c06b19193ad3c3159551966c05f48ea5" + }, + "internal/ratelimit/ratelimit.go": { + "mtime": 1786207185.0, + "ast_hash": "4d5bf0b77d390cf3e71abb4d022cf0aa", + "semantic_hash": "4d5bf0b77d390cf3e71abb4d022cf0aa" + }, + "internal/sieve/interp.go": { + "mtime": 1786169049.0, + "ast_hash": "6f13e1891ef551ffebba37e759742aca", + "semantic_hash": "6f13e1891ef551ffebba37e759742aca" + }, + "internal/sieve/lexer.go": { + "mtime": 1786168959.0, + "ast_hash": "3d8d9f9f203855eaf8d638935f65f7bb", + "semantic_hash": "3d8d9f9f203855eaf8d638935f65f7bb" + }, + "internal/sieve/parser.go": { + "mtime": 1786168977.0, + "ast_hash": "cbcd550894b5c473a160481098151605", + "semantic_hash": "cbcd550894b5c473a160481098151605" + }, + "internal/sieve/sieve_fuzz_test.go": { + "mtime": 1786207613.0, + "ast_hash": "fa017deb29e67fa5264e3ecc59ede127", + "semantic_hash": "fa017deb29e67fa5264e3ecc59ede127" + }, + "internal/smtp/auth.go": { + "mtime": 1786281931.1489654, + "ast_hash": "9220929f9afd4b5b8c39ee7401ea3ecd", + "semantic_hash": "9220929f9afd4b5b8c39ee7401ea3ecd" + }, + "internal/smtp/server.go": { + "mtime": 1786281931.153782, + "ast_hash": "30d88aa535f6ee39072f35fa6c1fd108", + "semantic_hash": "30d88aa535f6ee39072f35fa6c1fd108" + }, + "internal/smtp/session.go": { + "mtime": 1786281931.1489654, + "ast_hash": "41aae3368dc4be4eba559f0d732e9a37", + "semantic_hash": "41aae3368dc4be4eba559f0d732e9a37" + }, + "internal/tlsutil/acme_manager.go": { + "mtime": 1786281902.8497763, + "ast_hash": "5e7aa10c39162f5d95f927ba4efc30fb", + "semantic_hash": "5e7aa10c39162f5d95f927ba4efc30fb" + }, + "internal/tlsutil/selfsigned.go": { + "mtime": 1786193053.0, + "ast_hash": "9c58f78ed11e4c7bf208306285f0b32e", + "semantic_hash": "9c58f78ed11e4c7bf208306285f0b32e" + }, + "internal/totp/totp.go": { + "mtime": 1786193280.0, + "ast_hash": "48f2c9db1c1307f13d78726aa3b24f6a", + "semantic_hash": "48f2c9db1c1307f13d78726aa3b24f6a" + }, + "internal/vcard/vcard.go": { + "mtime": 1785905650.0, + "ast_hash": "8b0043626bac3cacc11b6ce76259445b", + "semantic_hash": "8b0043626bac3cacc11b6ce76259445b" + }, + "internal/vcard/vcard_fuzz_test.go": { + "mtime": 1786207583.0, + "ast_hash": "5aeab637a3c3357bb2c13cacc21508c6", + "semantic_hash": "5aeab637a3c3357bb2c13cacc21508c6" + }, + "internal/webmail/api.go": { + "mtime": 1786281903.8315737, + "ast_hash": "3ccccaaef2a926a90919805f67173db2", + "semantic_hash": "3ccccaaef2a926a90919805f67173db2" + }, + "internal/webmail/embed.go": { + "mtime": 1786132251.0, + "ast_hash": "441b827ff1643c3807b6efb92119a721", + "semantic_hash": "441b827ff1643c3807b6efb92119a721" + }, + "internal/webtoken/webtoken.go": { + "mtime": 1786193462.0, + "ast_hash": "d94a2b905b20ccbd8a83e258245ba375", + "semantic_hash": "d94a2b905b20ccbd8a83e258245ba375" + }, + ".claude/go-web-app-no-deps-SKILL.md": { + "mtime": 1786206554.0, + "ast_hash": "eb54ace954e819a069a9f1284485d213", + "semantic_hash": "eb54ace954e819a069a9f1284485d213" + }, + ".claude/go-web-app-no-deps.skill": { + "mtime": 1786206554.0, + "ast_hash": "de820100f8ad2da76d57884262c9f5f0", + "semantic_hash": "de820100f8ad2da76d57884262c9f5f0" + }, + ".claude/iterative-build-discipline-SKILL.md": { + "mtime": 1786206504.0, + "ast_hash": "d0e94862b77af5f6ac99c3f2668db3c9", + "semantic_hash": "d0e94862b77af5f6ac99c3f2668db3c9" + }, + ".claude/iterative-build-discipline.skill": { + "mtime": 1786206504.0, + "ast_hash": "87a25b626d37a9dfe5c037ae409fecad", + "semantic_hash": "87a25b626d37a9dfe5c037ae409fecad" + }, + "CLAUDE.md": { + "mtime": 1786281487.0884602, + "ast_hash": "82efb97a359f5c7290bf2513d14686be", + "semantic_hash": "82efb97a359f5c7290bf2513d14686be" + }, + "GOMAIL_HANDOVER.md": { + "mtime": 1786206496.0, + "ast_hash": "81fbcb0478fce27c6b6e8c6646eed44a", + "semantic_hash": "81fbcb0478fce27c6b6e8c6646eed44a" + }, + "README.md": { + "mtime": 1786208021.0, + "ast_hash": "257dbe428b6a41b51d4d8efcd2431615", + "semantic_hash": "257dbe428b6a41b51d4d8efcd2431615" + }, + "gomail-action-plan-v4.md": { + "mtime": 1786206496.0, + "ast_hash": "203046a9566fb3c392ed2e51aa178805", + "semantic_hash": "203046a9566fb3c392ed2e51aa178805" + }, + "internal/admin/static/index.html": { + "mtime": 1786188708.0, + "ast_hash": "77b0b0e42cf45a961571ee5e3a3a219a", + "semantic_hash": "77b0b0e42cf45a961571ee5e3a3a219a" + }, + "internal/webmail/static/index.html": { + "mtime": 1786132285.0, + "ast_hash": "3b77272b21933601eed33f3441d386cc", + "semantic_hash": "3b77272b21933601eed33f3441d386cc" + } +} \ No newline at end of file diff --git a/internal/accounts/link.go b/internal/accounts/link.go new file mode 100644 index 0000000..8360b35 --- /dev/null +++ b/internal/accounts/link.go @@ -0,0 +1,142 @@ +package accounts + +import ( + "encoding/json" + "fmt" + + "gomail/internal/crypto" + "gomail/internal/db" + "gomail/internal/oauth2" + "github.com/google/uuid" +) + +// LinkIMAPAccount registers a generic IMAP/SMTP account for a user, storing +// the password encrypted (same HKDF-per-record scheme as messages/contacts — +// "linked-account-cred" is the purpose namespace, keyed by the new account's +// own ID so a compromise of one linked account's credential doesn't expose +// any other record). +func LinkIMAPAccount(database *db.DB, mk *crypto.MasterKey, userID, displayName, email, password, + imapHost string, imapPort int, imapTLS string, + smtpHost string, smtpPort int, smtpTLS string) (*db.LinkedAccount, error) { + + accountID := uuid.NewString() + credJSON, err := json.Marshal(IMAPCredential{Password: password}) + if err != nil { + return nil, fmt.Errorf("marshaling credential: %w", err) + } + encCred, err := crypto.Encrypt(mk, accountID, "linked-account-cred", credJSON) + if err != nil { + return nil, fmt.Errorf("encrypting credential: %w", err) + } + + account := &db.LinkedAccount{ + ID: accountID, + UserID: userID, + Provider: db.ProviderIMAP, + DisplayName: displayName, + EmailAddress: email, + AuthType: db.AuthTypePassword, + IMAPHost: imapHost, + IMAPPort: imapPort, + IMAPTLS: imapTLS, + SMTPHost: smtpHost, + SMTPPort: smtpPort, + SMTPTLS: smtpTLS, + CredentialEnc: encCred, + Active: true, + } + if err := database.InsertLinkedAccount(account); err != nil { + return nil, err + } + return account, nil +} + +// wellKnownIMAPHost returns the fixed IMAP+SMTP host/port/TLS settings for +// Gmail and M365 — these aren't operator- or user-configurable, since +// they're the provider's actual documented endpoints, exactly like +// oauth2.WellKnownEndpoints. +func wellKnownIMAPHost(provider db.LinkedAccountProvider) (imapHost string, imapPort int, imapTLS, smtpHost string, smtpPort int, smtpTLS string, err error) { + switch provider { + case db.ProviderGmail: + return "imap.gmail.com", 993, "implicit", "smtp.gmail.com", 587, "starttls", nil + case db.ProviderM365: + return "outlook.office365.com", 993, "implicit", "smtp.office365.com", 587, "starttls", nil + default: + return "", 0, "", "", 0, "", fmt.Errorf("no well-known IMAP host for provider %q", provider) + } +} + +// LinkOAuth2Account registers a Gmail or M365 account, storing the OAuth2 +// tokens (from a completed authorization code exchange) encrypted the same +// way as everything else. Mail access goes through IMAP+OAuth2 (XOAUTH2), +// per the plan's decision to start there before adding native Gmail/Graph +// API push in a later pass — see accounts.IMAPProvider.loginOAuth2. +func LinkOAuth2Account(database *db.DB, mk *crypto.MasterKey, userID, displayName, email string, + provider db.LinkedAccountProvider, token *oauth2.Token) (*db.LinkedAccount, error) { + + imapHost, imapPort, imapTLS, smtpHost, smtpPort, smtpTLS, err := wellKnownIMAPHost(provider) + if err != nil { + return nil, err + } + + accountID := uuid.NewString() + credJSON, err := json.Marshal(OAuth2Credential{ + AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresAt: token.ExpiresAt, + }) + if err != nil { + return nil, fmt.Errorf("marshaling OAuth2 credential: %w", err) + } + encCred, err := crypto.Encrypt(mk, accountID, "linked-account-cred", credJSON) + if err != nil { + return nil, fmt.Errorf("encrypting OAuth2 credential: %w", err) + } + + expiresAt := token.ExpiresAt + account := &db.LinkedAccount{ + ID: accountID, + UserID: userID, + Provider: provider, + DisplayName: displayName, + EmailAddress: email, + AuthType: db.AuthTypeOAuth2, + IMAPHost: imapHost, + IMAPPort: imapPort, + IMAPTLS: imapTLS, + SMTPHost: smtpHost, + SMTPPort: smtpPort, + SMTPTLS: smtpTLS, + CredentialEnc: encCred, + OAuthExpiresAt: &expiresAt, + Active: true, + } + if err := database.InsertLinkedAccount(account); err != nil { + return nil, err + } + return account, nil +} + +// ProviderFor returns the right MailProvider implementation for a linked +// account row — the single place that decides which backend handles which +// provider string, so callers (webmail API, sync workers) never need a +// switch statement of their own. The local "gomail" provider isn't built +// through here since it needs a *db.User + *mailstore.Store, not a +// LinkedAccount row — callers construct it directly via NewGoMailProvider. +// +// oauthConfigs is keyed by provider name ("google", "microsoft") — only +// consulted for accounts.AuthTypeOAuth2 rows, to refresh an expired access +// token. Pass nil if the account is known to be password-based. +func ProviderFor(account *db.LinkedAccount, mk *crypto.MasterKey, database *db.DB, oauthConfigs map[string]*oauth2.Config) (MailProvider, error) { + switch account.Provider { + case db.ProviderIMAP: + return NewIMAPProvider(account, mk), nil + case db.ProviderGmail, db.ProviderM365: + providerKey := "google" + if account.Provider == db.ProviderM365 { + providerKey = "microsoft" + } + cfg := oauthConfigs[providerKey] + return NewIMAPProviderOAuth2(account, mk, database, cfg), nil + default: + return nil, fmt.Errorf("provider %q not supported", account.Provider) + } +} diff --git a/internal/accounts/provider.go b/internal/accounts/provider.go new file mode 100644 index 0000000..19601f6 --- /dev/null +++ b/internal/accounts/provider.go @@ -0,0 +1,67 @@ +// Package accounts implements the MailProvider abstraction: one interface, +// multiple backends (local GoMail account, generic IMAP/SMTP, and — Phase 10 — +// Gmail/M365 native APIs). The webmail client (later phase) talks to every +// linked account through this same interface regardless of where the mail +// actually lives. +package accounts + +import "context" + +type Folder struct { + ID string `json:"id"` // provider-native folder identifier (IMAP mailbox name, etc.) + DisplayName string `json:"display_name"` + Type string `json:"type"` // inbox|sent|drafts|trash|junk|custom + UnreadCount int `json:"unread_count"` + TotalCount int `json:"total_count"` +} + +type MessageHeader struct { + ID string `json:"id"` // provider-native message identifier (IMAP UID, etc.) + FolderID string `json:"folder_id"` + From string `json:"from"` + To string `json:"to"` + Subject string `json:"subject"` + Date string `json:"date"` + Flags []string `json:"flags"` + SizeBytes int64 `json:"size_bytes"` +} + +type FullMessage struct { + MessageHeader + Raw []byte `json:"raw"` // full RFC 5322 message; json.Marshal base64-encodes []byte automatically +} + +type OutgoingMessage struct { + From string `json:"from"` + To []string `json:"to"` + CC []string `json:"cc"` + BCC []string `json:"bcc"` + Subject string `json:"subject"` + Body string `json:"body"` // plain text; HTML composer is a webmail-phase concern +} + +type ListOpts struct { + Limit int + Offset int +} + +type SyncResult struct { + NewCursor string + NewMessages []MessageHeader + DeletedIDs []string + FlagsChanged map[string][]string // messageID -> new flags +} + +// MailProvider is implemented once per account type. Every method takes a +// context so network-backed implementations (IMAP, and later Gmail/Graph +// API) can be cancelled/timed-out uniformly with the local implementation. +type MailProvider interface { + ListFolders(ctx context.Context) ([]Folder, error) + ListMessages(ctx context.Context, folderID string, opts ListOpts) ([]MessageHeader, error) + GetMessage(ctx context.Context, folderID, messageID string) (*FullMessage, error) + SendMessage(ctx context.Context, msg *OutgoingMessage) error + SetFlags(ctx context.Context, folderID, messageID string, flags []string) error + Move(ctx context.Context, folderID, messageID, destFolderID string) error + Delete(ctx context.Context, folderID, messageID string) error + Sync(ctx context.Context, since string) (*SyncResult, error) +} diff --git a/internal/accounts/provider_gomail.go b/internal/accounts/provider_gomail.go new file mode 100644 index 0000000..dfd2505 --- /dev/null +++ b/internal/accounts/provider_gomail.go @@ -0,0 +1,256 @@ +package accounts + +import ( + "context" + "fmt" + "net/mail" + "strconv" + "strings" + "time" + + "gomail/internal/db" + "gomail/internal/mailstore" + "github.com/google/uuid" +) + +// GoMailProvider implements MailProvider for the user's own local account — +// direct function calls against the database and Maildir store, no network +// round-trip. This is what the webmail client uses for "your own" mailbox; +// Phase 9's JMAP server exposes the same data over HTTP for third-party +// clients, but the webmail's internal path stays this direct route since it's +// strictly faster for same-process access. +type GoMailProvider struct { + database *db.DB + store *mailstore.Store + user *db.User +} + +func NewGoMailProvider(database *db.DB, store *mailstore.Store, user *db.User) *GoMailProvider { + return &GoMailProvider{database: database, store: store, user: user} +} + +func (p *GoMailProvider) ListFolders(_ context.Context) ([]Folder, error) { + names, err := p.database.ListMailboxNames(p.user.ID) + if err != nil { + return nil, err + } + var folders []Folder + for _, name := range names { + entries, err := p.database.ListMailboxEntries(p.user.ID, name) + if err != nil { + continue + } + unread := 0 + for _, e := range entries { + if !strings.Contains(e.Flags, "\\Seen") { + unread++ + } + } + folders = append(folders, Folder{ + ID: name, + DisplayName: name, + Type: folderType(name), + UnreadCount: unread, + TotalCount: len(entries), + }) + } + return folders, nil +} + +func (p *GoMailProvider) ListMessages(_ context.Context, folderID string, opts ListOpts) ([]MessageHeader, error) { + entries, err := p.database.ListMailboxEntries(p.user.ID, folderID) + if err != nil { + return nil, err + } + + // Newest first, matching typical mail client default sort. + for i, j := 0, len(entries)-1; i < j; i, j = i+1, j-1 { + entries[i], entries[j] = entries[j], entries[i] + } + + start := opts.Offset + if start > len(entries) { + start = len(entries) + } + end := len(entries) + if opts.Limit > 0 && start+opts.Limit < end { + end = start + opts.Limit + } + page := entries[start:end] + + var headers []MessageHeader + for _, e := range page { + raw, err := p.store.Read(e.EMLPath) + if err != nil { + continue + } + headers = append(headers, headerFromRaw(strconv.Itoa(e.UID), folderID, raw, e.Flags, e.SizeBytes)) + } + return headers, nil +} + +func (p *GoMailProvider) GetMessage(_ context.Context, folderID, messageID string) (*FullMessage, error) { + entries, err := p.database.ListMailboxEntries(p.user.ID, folderID) + if err != nil { + return nil, err + } + uid, _ := strconv.Atoi(messageID) + for _, e := range entries { + if e.UID == uid { + raw, err := p.store.Read(e.EMLPath) + if err != nil { + return nil, err + } + hdr := headerFromRaw(messageID, folderID, raw, e.Flags, e.SizeBytes) + return &FullMessage{MessageHeader: hdr, Raw: raw}, nil + } + } + return nil, db.ErrNotFound +} + +// SendMessage delivers locally if the recipient is a GoMail user on this +// instance, otherwise stages it in the outbound queue — same routing logic +// SMTP submission uses, exposed here so webmail compose doesn't need to loop +// back through the SMTP port to send its own account's mail. +func (p *GoMailProvider) SendMessage(_ context.Context, msg *OutgoingMessage) error { + raw := buildRFC5322(p.user.Email, msg) + + for _, to := range msg.To { + domain := domainOf(to) + localDomain, err := p.database.LookupDomainByName(domain) + if err == nil && localDomain != nil { + if recipUser, err := p.database.LookupUserByEmail(to); err == nil { + if _, err := p.store.Deliver(recipUser.ID, recipUser.Email, "INBOX", raw); err != nil { + return fmt.Errorf("local delivery to %s failed: %w", to, err) + } + continue + } + } + + _, queuePath, err := p.store.WriteQueueFile(raw) + if err != nil { + return fmt.Errorf("staging outbound message: %w", err) + } + entry := &db.OutboundQueueEntry{ + ID: uuid.NewString(), + UserID: p.user.ID, + FromAddress: p.user.Email, + ToAddress: to, + EMLPath: queuePath, + NextAttemptAt: time.Now().UTC(), + } + if err := p.database.InsertOutboundQueueEntry(entry); err != nil { + return fmt.Errorf("enqueueing outbound message to %s: %w", to, err) + } + } + return nil +} + +func (p *GoMailProvider) SetFlags(_ context.Context, folderID, messageID string, flags []string) error { + entries, err := p.database.ListMailboxEntries(p.user.ID, folderID) + if err != nil { + return err + } + uid, _ := strconv.Atoi(messageID) + for _, e := range entries { + if e.UID == uid { + return p.database.UpdateMailboxFlags(e.ID, strings.Join(flags, " ")) + } + } + return db.ErrNotFound +} + +// Move re-delivers the message into the destination folder and removes it +// from the source — GoMail's Maildir store has no native "move" primitive +// (each mailbox is its own directory tree), so this is copy+delete rather +// than an atomic rename. Acceptable since both halves are local and fast; +// a future optimization could hardlink instead of re-encrypting. +func (p *GoMailProvider) Move(ctx context.Context, folderID, messageID, destFolderID string) error { + full, err := p.GetMessage(ctx, folderID, messageID) + if err != nil { + return err + } + if _, err := p.store.Deliver(p.user.ID, p.user.Email, destFolderID, full.Raw); err != nil { + return fmt.Errorf("delivering to destination folder: %w", err) + } + return p.Delete(ctx, folderID, messageID) +} + +func (p *GoMailProvider) Delete(_ context.Context, folderID, messageID string) error { + entries, err := p.database.ListMailboxEntries(p.user.ID, folderID) + if err != nil { + return err + } + uid, _ := strconv.Atoi(messageID) + for _, e := range entries { + if e.UID == uid { + return p.database.DeleteMailboxEntry(e.ID) + } + } + return db.ErrNotFound +} + +// Sync for the local provider is a no-op in the SyncResult sense — the +// caller already has live DB access via ListMessages, there's no remote +// state to reconcile. Implemented to satisfy the interface for callers that +// treat every linked account uniformly (the future unified-inbox sync loop). +func (p *GoMailProvider) Sync(_ context.Context, _ string) (*SyncResult, error) { + return &SyncResult{NewCursor: ""}, nil +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +func folderType(name string) string { + switch strings.ToUpper(name) { + case "INBOX": + return "inbox" + case "SENT": + return "sent" + case "DRAFTS": + return "drafts" + case "TRASH": + return "trash" + case "JUNK": + return "junk" + default: + return "custom" + } +} + +func headerFromRaw(id, folderID string, raw []byte, flags string, size int64) MessageHeader { + h := MessageHeader{ID: id, FolderID: folderID, SizeBytes: size} + if flags != "" { + h.Flags = strings.Fields(flags) + } + msg, err := mail.ReadMessage(strings.NewReader(string(raw))) + if err == nil { + h.From = msg.Header.Get("From") + h.To = msg.Header.Get("To") + h.Subject = msg.Header.Get("Subject") + h.Date = msg.Header.Get("Date") + } + return h +} + +func domainOf(email string) string { + parts := strings.SplitN(email, "@", 2) + if len(parts) == 2 { + return parts[1] + } + return "" +} + +func buildRFC5322(from string, msg *OutgoingMessage) []byte { + var b strings.Builder + b.WriteString("From: " + from + "\r\n") + b.WriteString("To: " + strings.Join(msg.To, ", ") + "\r\n") + if len(msg.CC) > 0 { + b.WriteString("Cc: " + strings.Join(msg.CC, ", ") + "\r\n") + } + b.WriteString("Subject: " + msg.Subject + "\r\n") + b.WriteString("Date: " + time.Now().Format(time.RFC1123Z) + "\r\n") + b.WriteString("\r\n") + b.WriteString(msg.Body) + b.WriteString("\r\n") + return []byte(b.String()) +} diff --git a/internal/accounts/provider_imap.go b/internal/accounts/provider_imap.go new file mode 100644 index 0000000..ecb7051 --- /dev/null +++ b/internal/accounts/provider_imap.go @@ -0,0 +1,322 @@ +package accounts + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net/mail" + "strconv" + "strings" + "time" + + "gomail/internal/crypto" + "gomail/internal/db" + "gomail/internal/imapclient" + "gomail/internal/oauth2" +) + +const dialTimeout = 20 * time.Second + +// IMAPCredential is what's stored (encrypted) in linked_accounts.credential_enc +// for AuthTypePassword accounts. +type IMAPCredential struct { + Password string `json:"password"` +} + +// OAuth2Credential is what's stored (encrypted) in +// linked_accounts.credential_enc for AuthTypeOAuth2 accounts (Gmail, M365, +// or any provider using XOAUTH2). RefreshToken is used to obtain a new +// AccessToken transparently once the stored one expires. +type OAuth2Credential struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt time.Time `json:"expires_at"` +} + +// IMAPProvider implements MailProvider for a generic external IMAP account. +// Every method dials fresh — IMAP has no cheap "keep a pool of idle +// connections" story without IDLE/pooling machinery this pass doesn't build +// yet, so simplicity wins: connect, do the one operation, disconnect. A +// later pass can add persistent connections if per-operation latency matters. +type IMAPProvider struct { + account *db.LinkedAccount + mk *crypto.MasterKey + database *db.DB // needed to persist a refreshed access token + oauthConfig *oauth2.Config // nil for password-auth accounts +} + +func NewIMAPProvider(account *db.LinkedAccount, mk *crypto.MasterKey) *IMAPProvider { + return &IMAPProvider{account: account, mk: mk} +} + +// NewIMAPProviderOAuth2 is used for accounts.AuthTypeOAuth2 — the caller +// supplies the provider's oauth2.Config (built from operator-configured +// Client ID/Secret) so a stored access token can be refreshed transparently +// on expiry. database is used to persist the refreshed token — refreshing +// silently in memory only would force a re-refresh on every single +// operation instead of once per real expiry. +func NewIMAPProviderOAuth2(account *db.LinkedAccount, mk *crypto.MasterKey, database *db.DB, oauthConfig *oauth2.Config) *IMAPProvider { + return &IMAPProvider{account: account, mk: mk, database: database, oauthConfig: oauthConfig} +} + +func (p *IMAPProvider) connect(ctx context.Context) (*imapclient.Client, error) { + addr := fmt.Sprintf("%s:%d", p.account.IMAPHost, p.account.IMAPPort) + // InsecureSkipVerify is a known gap, not a silent one: real ACME + // issuance now exists (internal/acme, Phase 13), but this instance's own + // IMAP server still falls back to a self-signed cert whenever the + // operator hasn't configured tls.acme_domains — and a user's *other* + // linked IMAP server (their actual Gmail/M365/self-hosted account) is + // entirely outside our control regardless. Real clients solve the + // latter with an explicit "accept this certificate" trust step + // (pinning by fingerprint) — that UI flow belongs in the webmail + // account-linking phase, not here. Tracked as a gap, not swept under + // the rug. + tlsConf := &tls.Config{ServerName: p.account.IMAPHost, InsecureSkipVerify: true} + + implicit := p.account.IMAPTLS == "implicit" + client, err := imapclient.Dial(addr, implicit, tlsConf, dialTimeout) + if err != nil { + return nil, err + } + if p.account.IMAPTLS == "starttls" { + if err := client.StartTLS(tlsConf); err != nil { + return nil, fmt.Errorf("STARTTLS: %w", err) + } + } + + if p.account.AuthType == db.AuthTypeOAuth2 { + if err := p.loginOAuth2(ctx, client); err != nil { + return nil, err + } + return client, nil + } + + plain, err := crypto.Decrypt(p.mk, p.account.ID, "linked-account-cred", p.account.CredentialEnc) + if err != nil { + return nil, fmt.Errorf("decrypting stored credential: %w", err) + } + var cred IMAPCredential + if err := json.Unmarshal(plain, &cred); err != nil { + return nil, fmt.Errorf("parsing stored credential: %w", err) + } + if err := client.Login(p.account.EmailAddress, cred.Password); err != nil { + return nil, fmt.Errorf("IMAP login: %w", err) + } + return client, nil +} + +// loginOAuth2 decrypts the stored OAuth2 credential, transparently refreshes +// it if expired (persisting the new token so the next call doesn't have to +// refresh again), and authenticates via SASL XOAUTH2. +func (p *IMAPProvider) loginOAuth2(ctx context.Context, client *imapclient.Client) error { + plain, err := crypto.Decrypt(p.mk, p.account.ID, "linked-account-cred", p.account.CredentialEnc) + if err != nil { + return fmt.Errorf("decrypting stored OAuth2 credential: %w", err) + } + var cred OAuth2Credential + if err := json.Unmarshal(plain, &cred); err != nil { + return fmt.Errorf("parsing stored OAuth2 credential: %w", err) + } + + if time.Now().UTC().After(cred.ExpiresAt) { + if p.oauthConfig == nil { + return fmt.Errorf("access token expired and no oauth2.Config available to refresh it") + } + newTok, err := p.oauthConfig.RefreshToken(ctx, cred.RefreshToken) + if err != nil { + return fmt.Errorf("refreshing OAuth2 token: %w", err) + } + cred.AccessToken = newTok.AccessToken + cred.RefreshToken = newTok.RefreshToken + cred.ExpiresAt = newTok.ExpiresAt + + if p.database != nil { + updated, err := json.Marshal(cred) + if err == nil { + if encUpdated, encErr := crypto.Encrypt(p.mk, p.account.ID, "linked-account-cred", updated); encErr == nil { + p.database.Exec(`UPDATE linked_accounts SET credential_enc = ?, oauth_expires_at = ? WHERE id = ?`, + encUpdated, cred.ExpiresAt, p.account.ID) + } + } + } + } + + sasl := oauth2.XOAUTH2SASLString(p.account.EmailAddress, cred.AccessToken) + if err := client.LoginXOAUTH2(sasl); err != nil { + return fmt.Errorf("XOAUTH2 login: %w", err) + } + return nil +} + +func (p *IMAPProvider) ListFolders(ctx context.Context) ([]Folder, error) { + client, err := p.connect(ctx) + if err != nil { + return nil, err + } + defer client.Logout() + + list, err := client.List() + if err != nil { + return nil, err + } + + var folders []Folder + for _, f := range list { + info, err := client.Select(f.Name) + total := 0 + if err == nil { + total = info.Exists + } + folders = append(folders, Folder{ + ID: f.Name, DisplayName: f.Name, Type: folderType(f.Name), TotalCount: total, + }) + } + return folders, nil +} + +func (p *IMAPProvider) ListMessages(ctx context.Context, folderID string, opts ListOpts) ([]MessageHeader, error) { + client, err := p.connect(ctx) + if err != nil { + return nil, err + } + defer client.Logout() + + info, err := client.Select(folderID) + if err != nil { + return nil, err + } + if info.Exists == 0 { + return nil, nil + } + + limit := opts.Limit + if limit <= 0 || limit > info.Exists { + limit = info.Exists + } + lo := info.Exists - limit + 1 - opts.Offset + if lo < 1 { + lo = 1 + } + hi := info.Exists - opts.Offset + if hi < lo { + return nil, nil + } + seqSet := fmt.Sprintf("%d:%d", lo, hi) + + fetched, err := client.Fetch(seqSet, "(UID FLAGS BODY.PEEK[HEADER])") + if err != nil { + return nil, err + } + + var headers []MessageHeader + for _, f := range fetched { + h := MessageHeader{ + ID: strconv.Itoa(f.UID), + FolderID: folderID, + Flags: f.Flags, + } + if msg, err := mail.ReadMessage(strings.NewReader(string(f.Body))); err == nil { + h.From = msg.Header.Get("From") + h.To = msg.Header.Get("To") + h.Subject = msg.Header.Get("Subject") + h.Date = msg.Header.Get("Date") + } + headers = append(headers, h) + } + return headers, nil +} + +func (p *IMAPProvider) GetMessage(ctx context.Context, folderID, messageID string) (*FullMessage, error) { + client, err := p.connect(ctx) + if err != nil { + return nil, err + } + defer client.Logout() + + if _, err := client.Select(folderID); err != nil { + return nil, err + } + + fetched, err := client.UIDFetch(messageID, "(UID FLAGS BODY.PEEK[])") + if err != nil { + return nil, err + } + if len(fetched) == 0 { + return nil, db.ErrNotFound + } + f := fetched[0] + + h := MessageHeader{ID: messageID, FolderID: folderID, Flags: f.Flags, SizeBytes: int64(len(f.Body))} + if msg, err := mail.ReadMessage(strings.NewReader(string(f.Body))); err == nil { + h.From = msg.Header.Get("From") + h.To = msg.Header.Get("To") + h.Subject = msg.Header.Get("Subject") + h.Date = msg.Header.Get("Date") + } + return &FullMessage{MessageHeader: h, Raw: f.Body}, nil +} + +// SendMessage for a generic IMAP account routes through its paired SMTP +// settings — IMAP itself has no send capability, so this dials the account's +// smtp_host/port using the same stored credential, via stdlib net/smtp, +// mirroring the approach in internal/queue's MXDeliverer. +func (p *IMAPProvider) SendMessage(_ context.Context, msg *OutgoingMessage) error { + return sendViaSMTP(p.account, p.mk, msg) +} + +func (p *IMAPProvider) SetFlags(ctx context.Context, folderID, messageID string, flags []string) error { + client, err := p.connect(ctx) + if err != nil { + return err + } + defer client.Logout() + + if _, err := client.Select(folderID); err != nil { + return err + } + return client.UIDStore(messageID, "FLAGS", strings.Join(flags, " ")) +} + +func (p *IMAPProvider) Move(ctx context.Context, folderID, messageID, destFolderID string) error { + // No native IMAP MOVE issued here (RFC 6851 COPY+EXPUNGE equivalent) — + // implemented as fetch-from-source + append-style re-delivery is not + // available without APPEND support (deferred). For now: mark \Deleted + // and expunge in the source; true cross-folder move needs APPEND, noted + // as a gap rather than silently mis-behaving. + return fmt.Errorf("Move not yet implemented for generic IMAP accounts (requires APPEND, deferred)") +} + +func (p *IMAPProvider) Delete(ctx context.Context, folderID, messageID string) error { + client, err := p.connect(ctx) + if err != nil { + return err + } + defer client.Logout() + + if _, err := client.Select(folderID); err != nil { + return err + } + if err := client.UIDStore(messageID, "+FLAGS", `\Deleted`); err != nil { + return err + } + return client.Expunge() +} + +func (p *IMAPProvider) Sync(ctx context.Context, _ string) (*SyncResult, error) { + // Full re-list — no CONDSTORE/QRESYNC support yet (deferred, noted in + // package docs). Correct, just not incremental. + folders, err := p.ListFolders(ctx) + if err != nil { + return nil, err + } + var all []MessageHeader + for _, f := range folders { + msgs, err := p.ListMessages(ctx, f.ID, ListOpts{}) + if err != nil { + continue + } + all = append(all, msgs...) + } + return &SyncResult{NewMessages: all}, nil +} diff --git a/internal/accounts/provider_smtp_helper.go b/internal/accounts/provider_smtp_helper.go new file mode 100644 index 0000000..4ec89fa --- /dev/null +++ b/internal/accounts/provider_smtp_helper.go @@ -0,0 +1,81 @@ +package accounts + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/smtp" + "strconv" + + "gomail/internal/crypto" + "gomail/internal/db" +) + +// sendViaSMTP delivers an outgoing message through a linked account's own +// SMTP settings — stdlib net/smtp, same choice as internal/queue's +// MXDeliverer, so outbound for both "GoMail relays for me" and "I'm using my +// own IMAP+SMTP provider" stay on the same dependency-free foundation. +func sendViaSMTP(account *db.LinkedAccount, mk *crypto.MasterKey, msg *OutgoingMessage) error { + plain, err := crypto.Decrypt(mk, account.ID, "linked-account-cred", account.CredentialEnc) + if err != nil { + return fmt.Errorf("decrypting stored credential: %w", err) + } + var cred IMAPCredential // same password shape reused for the paired SMTP auth + if err := json.Unmarshal(plain, &cred); err != nil { + return fmt.Errorf("parsing stored credential: %w", err) + } + + addr := net.JoinHostPort(account.SMTPHost, strconv.Itoa(account.SMTPPort)) + conn, err := net.DialTimeout("tcp", addr, dialTimeout) + if err != nil { + return fmt.Errorf("dial %s: %w", addr, err) + } + defer conn.Close() + + if account.SMTPTLS == "implicit" { + // See provider_imap.go's connect() comment — same gap, same reason. + conn = tls.Client(conn, &tls.Config{ServerName: account.SMTPHost, InsecureSkipVerify: true}) + } + + client, err := smtp.NewClient(conn, account.SMTPHost) + if err != nil { + return fmt.Errorf("SMTP handshake: %w", err) + } + defer client.Close() + + if account.SMTPTLS == "starttls" { + if ok, _ := client.Extension("STARTTLS"); ok { + if err := client.StartTLS(&tls.Config{ServerName: account.SMTPHost}); err != nil { + return fmt.Errorf("STARTTLS: %w", err) + } + } + } + + auth := smtp.PlainAuth("", account.EmailAddress, cred.Password, account.SMTPHost) + if err := client.Auth(auth); err != nil { + return fmt.Errorf("SMTP auth: %w", err) + } + + if err := client.Mail(account.EmailAddress); err != nil { + return err + } + for _, to := range msg.To { + if err := client.Rcpt(to); err != nil { + return err + } + } + + w, err := client.Data() + if err != nil { + return err + } + raw := buildRFC5322(account.EmailAddress, msg) + if _, err := w.Write(raw); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + return client.Quit() +} diff --git a/internal/acme/challenge.go b/internal/acme/challenge.go new file mode 100644 index 0000000..90b2fce --- /dev/null +++ b/internal/acme/challenge.go @@ -0,0 +1,45 @@ +package acme + +import ( + "net/http" + "strings" + "sync" +) + +// ChallengeResponder serves HTTP-01 challenge responses at +// /.well-known/acme-challenge/{token} — mount it on the plain :80 listener +// (or wherever the CA's HTTP-01 validator will connect) before requesting +// challenge validation. +type ChallengeResponder struct { + mu sync.RWMutex + tokens map[string]string // token -> key authorization +} + +func NewChallengeResponder() *ChallengeResponder { + return &ChallengeResponder{tokens: make(map[string]string)} +} + +func (c *ChallengeResponder) Set(token, keyAuthorization string) { + c.mu.Lock() + defer c.mu.Unlock() + c.tokens[token] = keyAuthorization +} + +func (c *ChallengeResponder) Remove(token string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.tokens, token) +} + +func (c *ChallengeResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) { + token := strings.TrimPrefix(r.URL.Path, "/.well-known/acme-challenge/") + c.mu.RLock() + keyAuth, ok := c.tokens[token] + c.mu.RUnlock() + if !ok { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Write([]byte(keyAuth)) +} diff --git a/internal/acme/client.go b/internal/acme/client.go new file mode 100644 index 0000000..b1cc10b --- /dev/null +++ b/internal/acme/client.go @@ -0,0 +1,301 @@ +package acme + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net/http" + "time" +) + +type directory struct { + NewNonce string `json:"newNonce"` + NewAccount string `json:"newAccount"` + NewOrder string `json:"newOrder"` +} + +type Client struct { + directoryURL string + httpClient *http.Client + dir directory + accountKey *AccountKey + accountURL string + nonce string +} + +func NewClient(directoryURL string, accountKey *AccountKey) *Client { + return &Client{ + directoryURL: directoryURL, + httpClient: &http.Client{Timeout: 30 * time.Second}, + accountKey: accountKey, + } +} + +// Bootstrap fetches the directory and a fresh nonce — call once before any +// other method. +func (c *Client) Bootstrap() error { + resp, err := c.httpClient.Get(c.directoryURL) + if err != nil { + return fmt.Errorf("fetching ACME directory: %w", err) + } + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(&c.dir); err != nil { + return fmt.Errorf("parsing ACME directory: %w", err) + } + + nonceResp, err := c.httpClient.Head(c.dir.NewNonce) + if err != nil { + return fmt.Errorf("fetching initial nonce: %w", err) + } + defer nonceResp.Body.Close() + c.nonce = nonceResp.Header.Get("Replay-Nonce") + if c.nonce == "" { + return fmt.Errorf("server did not return a Replay-Nonce") + } + return nil +} + +// post sends a JWS-signed POST and captures the next nonce from the +// response for the following request — ACME nonces are single-use. +func (c *Client) post(url string, payload []byte) (*http.Response, []byte, error) { + useJWK := c.accountURL == "" + body, err := c.accountKey.signJWS(url, c.nonce, useJWK, c.accountURL, payload) + if err != nil { + return nil, nil, fmt.Errorf("signing request: %w", err) + } + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/jose+json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("ACME request to %s: %w", url, err) + } + defer resp.Body.Close() + + if n := resp.Header.Get("Replay-Nonce"); n != "" { + c.nonce = n + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return resp, nil, fmt.Errorf("reading response body: %w", err) + } + return resp, respBody, nil +} + +// NewAccount registers (or, per RFC 8555 §7.3.1, retrieves the existing +// account for this key if already registered) an ACME account. +func (c *Client) NewAccount(contactEmail string) error { + payload, err := json.Marshal(map[string]any{ + "termsOfServiceAgreed": true, + "contact": []string{"mailto:" + contactEmail}, + }) + if err != nil { + return err + } + + resp, body, err := c.post(c.dir.NewAccount, payload) + if err != nil { + return err + } + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + return fmt.Errorf("new-account failed: status %d: %s", resp.StatusCode, string(body)) + } + + c.accountURL = resp.Header.Get("Location") + if c.accountURL == "" { + return fmt.Errorf("server did not return an account URL") + } + return nil +} + +type Order struct { + Status string `json:"status"` + Authorizations []string `json:"authorizations"` + Finalize string `json:"finalize"` + Certificate string `json:"certificate"` + orderURL string +} + +func (c *Client) NewOrder(domains []string) (*Order, error) { + var idents []map[string]string + for _, d := range domains { + idents = append(idents, map[string]string{"type": "dns", "value": d}) + } + payload, err := json.Marshal(map[string]any{"identifiers": idents}) + if err != nil { + return nil, err + } + + resp, body, err := c.post(c.dir.NewOrder, payload) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("new-order failed: status %d: %s", resp.StatusCode, string(body)) + } + + var order Order + if err := json.Unmarshal(body, &order); err != nil { + return nil, fmt.Errorf("parsing order: %w", err) + } + order.orderURL = resp.Header.Get("Location") + return &order, nil +} + +type Authorization struct { + Status string `json:"status"` + Identifier struct { + Value string `json:"value"` + } `json:"identifier"` + Challenges []Challenge `json:"challenges"` +} + +type Challenge struct { + Type string `json:"type"` + URL string `json:"url"` + Token string `json:"token"` + Status string `json:"status"` +} + +// GetAuthorization fetches one authorization (POST-as-GET, per RFC 8555 §6.3). +func (c *Client) GetAuthorization(authzURL string) (*Authorization, error) { + resp, body, err := c.post(authzURL, nil) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("get authorization failed: status %d: %s", resp.StatusCode, string(body)) + } + var authz Authorization + if err := json.Unmarshal(body, &authz); err != nil { + return nil, fmt.Errorf("parsing authorization: %w", err) + } + return &authz, nil +} + +// KeyAuthorization builds the value the HTTP-01 challenge response must +// serve at /.well-known/acme-challenge/{token} — the token plus a JWK +// thumbprint of the account key, per RFC 8555 §8.3. +func (c *Client) KeyAuthorization(token string) string { + return token + "." + c.accountKey.thumbprint() +} + +// RespondToChallenge tells the server the challenge is ready to be +// validated — the caller must have already made the key authorization +// available at the HTTP-01 well-known path before calling this. +func (c *Client) RespondToChallenge(challengeURL string) error { + resp, body, err := c.post(challengeURL, []byte("{}")) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("challenge response failed: status %d: %s", resp.StatusCode, string(body)) + } + return nil +} + +// WaitForAuthorizationValid polls an authorization until it's valid, +// invalid, or the timeout elapses. +func (c *Client) WaitForAuthorizationValid(authzURL string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + authz, err := c.GetAuthorization(authzURL) + if err != nil { + return err + } + switch authz.Status { + case "valid": + return nil + case "invalid": + return fmt.Errorf("authorization for %s became invalid", authz.Identifier.Value) + } + time.Sleep(200 * time.Millisecond) + } + return fmt.Errorf("timed out waiting for authorization to become valid") +} + +// FinalizeAndDownload generates a fresh certificate key pair, builds and +// submits a CSR, polls the order until the certificate is issued, and +// downloads it — returning the PEM-encoded cert chain and the PEM-encoded +// private key for the certificate (distinct from the ACME account key). +func (c *Client) FinalizeAndDownload(order *Order, domains []string, timeout time.Duration) (certPEM, keyPEM []byte, err error) { + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("generating certificate key: %w", err) + } + + csrDER, err := buildCSR(certKey, domains) + if err != nil { + return nil, nil, fmt.Errorf("building CSR: %w", err) + } + + payload, err := json.Marshal(map[string]string{"csr": b64(csrDER)}) + if err != nil { + return nil, nil, err + } + resp, body, err := c.post(order.Finalize, payload) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("finalize failed: status %d: %s", resp.StatusCode, string(body)) + } + + var finalized Order + if err := json.Unmarshal(body, &finalized); err != nil { + return nil, nil, fmt.Errorf("parsing finalized order: %w", err) + } + finalized.orderURL = order.orderURL + + deadline := time.Now().Add(timeout) + for finalized.Status != "valid" { + if time.Now().After(deadline) { + return nil, nil, fmt.Errorf("timed out waiting for order to become valid (status: %s)", finalized.Status) + } + time.Sleep(200 * time.Millisecond) + _, pollBody, err := c.post(finalized.orderURL, nil) + if err != nil { + return nil, nil, err + } + if err := json.Unmarshal(pollBody, &finalized); err != nil { + return nil, nil, fmt.Errorf("parsing polled order: %w", err) + } + finalized.orderURL = order.orderURL + } + + certResp, certBody, err := c.post(finalized.Certificate, nil) + if err != nil { + return nil, nil, err + } + if certResp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("certificate download failed: status %d", certResp.StatusCode) + } + + keyDER, err := x509.MarshalECPrivateKey(certKey) + if err != nil { + return nil, nil, fmt.Errorf("marshaling certificate key: %w", err) + } + keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + return certBody, keyPEM, nil +} + +func buildCSR(key *ecdsa.PrivateKey, domains []string) ([]byte, error) { + template := &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: domains[0]}, + DNSNames: domains, + } + return x509.CreateCertificateRequest(rand.Reader, template, key) +} diff --git a/internal/acme/jws.go b/internal/acme/jws.go new file mode 100644 index 0000000..a1cce02 --- /dev/null +++ b/internal/acme/jws.go @@ -0,0 +1,143 @@ +// Package acme implements an ACME v2 (RFC 8555) client — account +// registration, order creation, HTTP-01 challenge response, and +// certificate issuance/renewal. Hand-rolled on stdlib crypto/ecdsa + +// encoding/json + net/http, including the JWS request signing ACME +// requires (RFC 7515 subset: ES256 only, flattened JSON serialization) — +// no third-party ACME or JOSE library, matching the project's +// dependency-minimal principle. +package acme + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" +) + +// AccountKey wraps the ECDSA P-256 key pair ACME accounts are identified +// by — generated once per hosted domain (or per instance) and stored +// encrypted, same pattern as DKIM keys. +type AccountKey struct { + Private *ecdsa.PrivateKey +} + +func GenerateAccountKey() (*AccountKey, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generating ACME account key: %w", err) + } + return &AccountKey{Private: priv}, nil +} + +func (k *AccountKey) MarshalPEM() ([]byte, error) { + der, err := x509.MarshalECPrivateKey(k.Private) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}), nil +} + +func ParseAccountKeyPEM(pemBytes []byte) (*AccountKey, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("no PEM block found") + } + priv, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing EC private key: %w", err) + } + return &AccountKey{Private: priv}, nil +} + +// jwk is the JSON Web Key representation of the account's public key — +// required in the JWS protected header for the very first request +// (new-account), before the server has assigned an account URL (kid). +type jwk struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` +} + +func (k *AccountKey) jwkValue() jwk { + size := 32 // P-256 coordinate size in bytes + return jwk{ + Kty: "EC", Crv: "P-256", + X: b64(leftPad(k.Private.X.Bytes(), size)), + Y: b64(leftPad(k.Private.Y.Bytes(), size)), + } +} + +// thumbprint computes the JWK thumbprint (RFC 7638) — used as the +// "key authorization" suffix for HTTP-01 challenge responses. +func (k *AccountKey) thumbprint() string { + j := k.jwkValue() + // RFC 7638 requires this EXACT key order and no extra whitespace. + canonical := fmt.Sprintf(`{"crv":"%s","kty":"%s","x":"%s","y":"%s"}`, j.Crv, j.Kty, j.X, j.Y) + sum := sha256.Sum256([]byte(canonical)) + return b64(sum[:]) +} + +// signJWS builds a flattened-JSON-serialization JWS per RFC 7515, signed +// with ES256, for one ACME request. Exactly one of useJWK/kid applies: +// useJWK for the very first request (new-account), kid for every request +// after (identifying the now-registered account by URL). +func (k *AccountKey) signJWS(url, nonce string, useJWK bool, kid string, payload []byte) ([]byte, error) { + protected := map[string]any{ + "alg": "ES256", + "nonce": nonce, + "url": url, + } + if useJWK { + protected["jwk"] = k.jwkValue() + } else { + protected["kid"] = kid + } + + protectedJSON, err := json.Marshal(protected) + if err != nil { + return nil, fmt.Errorf("marshaling protected header: %w", err) + } + protectedB64 := b64(protectedJSON) + + var payloadB64 string + if payload != nil { + payloadB64 = b64(payload) + } + // A nil payload (POST-as-GET requests) intentionally encodes as "" — + // not "null" — per RFC 8555 §6.3. + + signingInput := protectedB64 + "." + payloadB64 + hash := sha256.Sum256([]byte(signingInput)) + + r, s, err := ecdsa.Sign(rand.Reader, k.Private, hash[:]) + if err != nil { + return nil, fmt.Errorf("signing: %w", err) + } + sigBytes := append(leftPad(r.Bytes(), 32), leftPad(s.Bytes(), 32)...) + + jwsBody := map[string]string{ + "protected": protectedB64, + "payload": payloadB64, + "signature": b64(sigBytes), + } + return json.Marshal(jwsBody) +} + +func b64(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} + +func leftPad(b []byte, size int) []byte { + if len(b) >= size { + return b + } + out := make([]byte, size) + copy(out[size-len(b):], b) + return out +} diff --git a/internal/acme/obtain.go b/internal/acme/obtain.go new file mode 100644 index 0000000..db8c418 --- /dev/null +++ b/internal/acme/obtain.go @@ -0,0 +1,67 @@ +package acme + +import ( + "fmt" + "time" +) + +// Obtain drives the complete ACME issuance flow for one or more domains: +// bootstrap, account registration, order, HTTP-01 challenge response via +// responder, finalize, download. The caller is responsible for mounting +// responder on a listener the CA's HTTP-01 validator can reach at +// http://{domain}/.well-known/acme-challenge/{token} — this function only +// populates the token->response map, it doesn't start any listener itself. +func Obtain(directoryURL, contactEmail string, domains []string, accountKey *AccountKey, responder *ChallengeResponder) (certPEM, keyPEM []byte, err error) { + client := NewClient(directoryURL, accountKey) + if err := client.Bootstrap(); err != nil { + return nil, nil, fmt.Errorf("bootstrap: %w", err) + } + if err := client.NewAccount(contactEmail); err != nil { + return nil, nil, fmt.Errorf("account registration: %w", err) + } + + order, err := client.NewOrder(domains) + if err != nil { + return nil, nil, fmt.Errorf("creating order: %w", err) + } + + for _, authzURL := range order.Authorizations { + authz, err := client.GetAuthorization(authzURL) + if err != nil { + return nil, nil, fmt.Errorf("fetching authorization: %w", err) + } + if authz.Status == "valid" { + continue // already satisfied (e.g. from a very recent prior order) + } + + var httpChallenge *Challenge + for i := range authz.Challenges { + if authz.Challenges[i].Type == "http-01" { + httpChallenge = &authz.Challenges[i] + break + } + } + if httpChallenge == nil { + return nil, nil, fmt.Errorf("no http-01 challenge offered for %s", authz.Identifier.Value) + } + + keyAuth := client.KeyAuthorization(httpChallenge.Token) + responder.Set(httpChallenge.Token, keyAuth) + + if err := client.RespondToChallenge(httpChallenge.URL); err != nil { + responder.Remove(httpChallenge.Token) + return nil, nil, fmt.Errorf("responding to challenge for %s: %w", authz.Identifier.Value, err) + } + waitErr := client.WaitForAuthorizationValid(authzURL, 30*time.Second) + responder.Remove(httpChallenge.Token) + if waitErr != nil { + return nil, nil, fmt.Errorf("waiting for validation of %s: %w", authz.Identifier.Value, waitErr) + } + } + + certPEM, keyPEM, err = client.FinalizeAndDownload(order, domains, 30*time.Second) + if err != nil { + return nil, nil, fmt.Errorf("finalize/download: %w", err) + } + return certPEM, keyPEM, nil +} diff --git a/internal/admin/api.go b/internal/admin/api.go new file mode 100644 index 0000000..c6b56da --- /dev/null +++ b/internal/admin/api.go @@ -0,0 +1,311 @@ +// Package admin implements the admin portal REST API — domains (with DKIM +// key generation), tenants, users, list rules, outbound queue management, +// global quarantine, and dashboard stats. Reuses webtoken for sessions +// (same JWT scheme as webmail) but enforces role-based access: only +// global_admin and tenant_admin roles may authenticate here at all, and +// tenant_admin is scoped to their own tenant for every operation. +package admin + +import ( + "encoding/json" + "net/http" + "strings" + "time" + + "gomail/internal/auth" + "gomail/internal/crypto" + "gomail/internal/db" + "gomail/internal/dkim" + "gomail/internal/webtoken" + "github.com/google/uuid" +) + +const sessionTTL = 24 * time.Hour + +type Handler struct { + database *db.DB + mk *crypto.MasterKey + jwtSecret string +} + +func NewHandler(database *db.DB, mk *crypto.MasterKey, jwtSecret string) *Handler { + return &Handler{database: database, mk: mk, jwtSecret: jwtSecret} +} + +func (h *Handler) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/api/admin/auth/login", h.login) + mux.HandleFunc("/api/admin/stats", h.withAdmin(h.stats)) + mux.HandleFunc("/api/admin/tenants", h.withAdmin(h.tenants)) + mux.HandleFunc("/api/admin/domains", h.withAdmin(h.domains)) + mux.HandleFunc("/api/admin/domains/", h.withAdmin(h.domainByID)) + mux.HandleFunc("/api/admin/users", h.withAdmin(h.users)) + mux.HandleFunc("/api/admin/users/", h.withAdmin(h.userByID)) + mux.HandleFunc("/api/admin/list-rules", h.withAdmin(h.listRules)) + mux.HandleFunc("/api/admin/list-rules/", h.withAdmin(h.listRuleByID)) + mux.HandleFunc("/api/admin/queue", h.withAdmin(h.queue)) + mux.HandleFunc("/api/admin/queue/", h.withAdmin(h.queueByID)) + mux.HandleFunc("/api/admin/quarantine", h.withAdmin(h.quarantine)) + mux.HandleFunc("/api/admin/quarantine/", h.withAdmin(h.quarantineByID)) +} + +// ── JSON helpers ────────────────────────────────────────────────────────────── + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(v) +} + +func writeErr(w http.ResponseWriter, code int, msg string) { + writeJSON(w, code, map[string]string{"error": msg}) +} + +// ── Auth (admin-only roles) ───────────────────────────────────────────────────── + +func (h *Handler) login(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Email, Password string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + + user, ok := auth.Authenticate(h.database, req.Email, req.Password, auth.ScopeIMAP) + if !ok { + writeErr(w, http.StatusUnauthorized, "invalid credentials") + return + } + if user.Role != db.RoleGlobalAdmin && user.Role != db.RoleTenantAdmin { + // Deliberately the same error as bad credentials — don't leak "this + // account exists but lacks admin rights" to an unauthenticated caller. + writeErr(w, http.StatusUnauthorized, "invalid credentials") + return + } + + token, err := webtoken.Issue(h.jwtSecret, user.ID, user.TenantID, string(user.Role), sessionTTL) + if err != nil { + writeErr(w, http.StatusInternalServerError, "token generation failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "token": token, + "user": map[string]any{"id": user.ID, "email": user.Email, "role": user.Role}, + }) +} + +func (h *Handler) withAdmin(next func(http.ResponseWriter, *http.Request, *db.User)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tokenStr := "" + if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { + tokenStr = strings.TrimPrefix(authHeader, "Bearer ") + } + if tokenStr == "" { + writeErr(w, http.StatusUnauthorized, "missing token") + return + } + claims, err := webtoken.Verify(h.jwtSecret, tokenStr) + if err != nil { + writeErr(w, http.StatusUnauthorized, "invalid or expired token") + return + } + if claims.Role != string(db.RoleGlobalAdmin) && claims.Role != string(db.RoleTenantAdmin) { + writeErr(w, http.StatusForbidden, "admin role required") + return + } + + user, err := h.database.GetUser(claims.Subject) + if err != nil || !user.Active { + writeErr(w, http.StatusUnauthorized, "user not found or inactive") + return + } + // Re-check role against the live DB row, not just the JWT claim — a + // demoted admin's existing token shouldn't keep working until it + // naturally expires. + if user.Role != db.RoleGlobalAdmin && user.Role != db.RoleTenantAdmin { + writeErr(w, http.StatusForbidden, "admin role required") + return + } + + next(w, r, user) + } +} + +func (h *Handler) encryptDKIMKey(domainID string, keyPEM []byte) ([]byte, error) { + return crypto.Encrypt(h.mk, domainID, "dkim-key", keyPEM) +} + +// ── Dashboard ───────────────────────────────────────────────────────────────── + +func (h *Handler) stats(w http.ResponseWriter, r *http.Request, user *db.User) { + s, err := h.database.GetStats() + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, s) +} + +// ── Tenants (global_admin only) ───────────────────────────────────────────────── + +func (h *Handler) tenants(w http.ResponseWriter, r *http.Request, user *db.User) { + if user.Role != db.RoleGlobalAdmin { + writeErr(w, http.StatusForbidden, "only global_admin may manage tenants") + return + } + switch r.Method { + case http.MethodGet: + list, err := h.database.ListTenants() + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, list) + case http.MethodPost: + var req struct{ Name, DisplayName string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { + writeErr(w, http.StatusBadRequest, "name is required") + return + } + t := &db.Tenant{ID: uuid.NewString(), Name: req.Name, DisplayName: req.DisplayName} + if err := h.database.CreateTenant(t); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusCreated, t) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// ── Domains ─────────────────────────────────────────────────────────────────── + +func (h *Handler) domains(w http.ResponseWriter, r *http.Request, user *db.User) { + switch r.Method { + case http.MethodGet: + all, err := h.database.ListDomains() + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, filterDomainsByTenant(all, scopeTenant(user))) + + case http.MethodPost: + var req struct{ Domain, TenantID string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Domain == "" { + writeErr(w, http.StatusBadRequest, "domain is required") + return + } + tenantID := req.TenantID + if user.Role != db.RoleGlobalAdmin { + tenantID = user.TenantID + } else if tenantID == "" { + tenantID = user.TenantID // global_admin defaults to their own tenant if unspecified + } + if tenantID == "" { + writeErr(w, http.StatusBadRequest, "tenant_id is required") + return + } + + domainID := uuid.NewString() + selector := "mail" + kp, err := dkim.GenerateKeyPair() + if err != nil { + writeErr(w, http.StatusInternalServerError, "DKIM key generation failed: "+err.Error()) + return + } + keyEnc, err := h.encryptDKIMKey(domainID, kp.PrivateKeyPEM) + if err != nil { + writeErr(w, http.StatusInternalServerError, "DKIM key encryption failed: "+err.Error()) + return + } + + d := &db.Domain{ID: domainID, TenantID: tenantID, Domain: req.Domain, DKIMSelector: selector, DKIMPrivateKeyEnc: keyEnc} + if err := h.database.CreateDomain(d); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusCreated, map[string]any{ + "domain": d, "dkim_dns_record": kp.DNSRecordValue, "dkim_dns_name": selector + "._domainkey." + req.Domain, + }) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (h *Handler) domainByID(w http.ResponseWriter, r *http.Request, user *db.User) { + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/admin/domains/"), "/") + id := parts[0] + action := "" + if len(parts) > 1 { + action = parts[1] + } + + d, err := h.database.GetDomain(id) + if err != nil { + writeErr(w, http.StatusNotFound, "domain not found") + return + } + if user.Role != db.RoleGlobalAdmin && d.TenantID != user.TenantID { + writeErr(w, http.StatusForbidden, "not your tenant's domain") + return + } + + switch { + case r.Method == http.MethodPost && action == "dkim-rotate": + kp, err := dkim.GenerateKeyPair() + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + encKey, err := h.encryptDKIMKey(d.ID, kp.PrivateKeyPEM) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + if err := h.database.UpdateDomainDKIMKey(d.ID, d.DKIMSelector, encKey); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{ + "dkim_dns_record": kp.DNSRecordValue, "dkim_dns_name": d.DKIMSelector + "._domainkey." + d.Domain, + }) + + case r.Method == http.MethodDelete && action == "": + if err := h.database.DeleteDomain(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "deleted"}) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +// scopeTenant returns the tenant ID a tenant_admin is restricted to, or "" +// for global_admin (meaning "all tenants, no filter"). +func scopeTenant(user *db.User) string { + if user.Role == db.RoleGlobalAdmin { + return "" + } + return user.TenantID +} + +func filterDomainsByTenant(all []db.Domain, tenantID string) []db.Domain { + if tenantID == "" { + return all + } + var out []db.Domain + for _, d := range all { + if d.TenantID == tenantID { + out = append(out, d) + } + } + return out +} diff --git a/internal/admin/embed.go b/internal/admin/embed.go new file mode 100644 index 0000000..bab3a11 --- /dev/null +++ b/internal/admin/embed.go @@ -0,0 +1,6 @@ +package admin + +import "embed" + +//go:embed static/index.html +var StaticFS embed.FS diff --git a/internal/admin/handlers.go b/internal/admin/handlers.go new file mode 100644 index 0000000..db5bf8a --- /dev/null +++ b/internal/admin/handlers.go @@ -0,0 +1,271 @@ +package admin + +import ( + "encoding/json" + "net/http" + "strings" + + "gomail/internal/db" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +// ── Users ───────────────────────────────────────────────────────────────────── + +func (h *Handler) users(w http.ResponseWriter, r *http.Request, user *db.User) { + switch r.Method { + case http.MethodGet: + list, err := h.database.ListUsers(scopeTenant(user)) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, list) + + case http.MethodPost: + var req struct { + Email, Password, DisplayName, Role, DomainID string + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" || req.Password == "" { + writeErr(w, http.StatusBadRequest, "email and password are required") + return + } + if len(req.Password) < 8 { + writeErr(w, http.StatusBadRequest, "password must be at least 8 characters") + return + } + if req.Role == "" { + req.Role = string(db.RoleUser) + } + tenantID := user.TenantID + if user.Role != db.RoleGlobalAdmin && req.Role != string(db.RoleUser) { + writeErr(w, http.StatusForbidden, "tenant_admin may only create regular users") + return + } + + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) + if err != nil { + writeErr(w, http.StatusInternalServerError, "password hashing failed") + return + } + + newUser := &db.User{ + ID: uuid.NewString(), TenantID: tenantID, DomainID: req.DomainID, + Email: req.Email, DisplayName: req.DisplayName, Role: db.UserRole(req.Role), + } + if err := h.database.CreateUser(newUser, string(hash)); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusCreated, newUser) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (h *Handler) userByID(w http.ResponseWriter, r *http.Request, user *db.User) { + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/admin/users/"), "/") + id := parts[0] + action := "" + if len(parts) > 1 { + action = parts[1] + } + + target, err := h.database.GetUser(id) + if err != nil { + writeErr(w, http.StatusNotFound, "user not found") + return + } + if user.Role != db.RoleGlobalAdmin && target.TenantID != user.TenantID { + writeErr(w, http.StatusForbidden, "not your tenant's user") + return + } + if user.Role != db.RoleGlobalAdmin && (target.Role == db.RoleGlobalAdmin || target.Role == db.RoleTenantAdmin) && target.ID != user.ID { + writeErr(w, http.StatusForbidden, "cannot modify an admin account") + return + } + + switch { + case r.Method == http.MethodPost && action == "suspend": + if err := h.database.SetUserActive(id, false); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "suspended"}) + + case r.Method == http.MethodPost && action == "activate": + if err := h.database.SetUserActive(id, true); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "activated"}) + + case r.Method == http.MethodPost && action == "reset-password": + var req struct{ NewPassword string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.NewPassword) < 8 { + writeErr(w, http.StatusBadRequest, "new_password must be at least 8 characters") + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12) + if err != nil { + writeErr(w, http.StatusInternalServerError, "hashing failed") + return + } + if err := h.database.SetUserPassword(id, string(hash)); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "password reset"}) + + case r.Method == http.MethodDelete && action == "": + if err := h.database.DeleteUser(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "deleted"}) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// ── List rules ──────────────────────────────────────────────────────────────── + +func (h *Handler) listRules(w http.ResponseWriter, r *http.Request, user *db.User) { + tenantID := user.TenantID + if user.Role == db.RoleGlobalAdmin { + if qt := r.URL.Query().Get("tenant_id"); qt != "" { + tenantID = qt + } + } + if tenantID == "" { + writeErr(w, http.StatusBadRequest, "tenant_id is required") + return + } + + switch r.Method { + case http.MethodGet: + rules, err := h.database.ListListRules(tenantID) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, rules) + + case http.MethodPost: + var req struct{ ListType, MatchType, Value, Note string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + if req.ListType != "allow" && req.ListType != "block" { + writeErr(w, http.StatusBadRequest, "list_type must be 'allow' or 'block'") + return + } + if req.MatchType != "email" && req.MatchType != "domain" { + writeErr(w, http.StatusBadRequest, "match_type must be 'email' or 'domain'") + return + } + if req.Value == "" { + writeErr(w, http.StatusBadRequest, "value is required") + return + } + rule := &db.ListRule{ + ID: uuid.NewString(), TenantID: tenantID, + ListType: db.ListRuleAction(req.ListType), MatchType: req.MatchType, Value: req.Value, Note: req.Note, + } + if err := h.database.CreateListRule(rule); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusCreated, rule) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (h *Handler) listRuleByID(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/admin/list-rules/") + if err := h.database.DeleteListRule(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "deleted"}) +} + +// ── Outbound queue ──────────────────────────────────────────────────────────── + +func (h *Handler) queue(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + entries, err := h.database.ListAllOutboundQueue() + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, entries) +} + +func (h *Handler) queueByID(w http.ResponseWriter, r *http.Request, user *db.User) { + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/admin/queue/"), "/") + id := parts[0] + action := "" + if len(parts) > 1 { + action = parts[1] + } + + switch { + case r.Method == http.MethodPost && action == "retry": + if err := h.database.RetryQueueEntryNow(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "scheduled for immediate retry"}) + + case r.Method == http.MethodDelete && action == "": + if err := h.database.DeleteOutboundEntry(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "cancelled"}) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// ── Global quarantine ───────────────────────────────────────────────────────── + +func (h *Handler) quarantine(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + entries, err := h.database.ListAllQuarantine() + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, entries) +} + +func (h *Handler) quarantineByID(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/admin/quarantine/") + if err := h.database.DeleteQuarantineEntry(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "discarded"}) +} diff --git a/internal/admin/static/index.html b/internal/admin/static/index.html new file mode 100644 index 0000000..5ea273c --- /dev/null +++ b/internal/admin/static/index.html @@ -0,0 +1,238 @@ + + + + + +GoMail Admin + + + + + + + + + + + + diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..d076014 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,83 @@ +// Package auth provides shared credential verification for every protocol +// that needs it (SMTP AUTH, IMAP LOGIN, POP3 USER/PASS) — a single source of +// truth for how a username/password pair maps to a user, so a future change +// (MFA enforcement, passkey-only accounts, lockout policy) only needs to +// land in one place. +package auth + +import ( + "log/slog" + "strings" + "time" + + "gomail/internal/db" + "golang.org/x/crypto/bcrypt" +) + +// Scope identifies which protocol is authenticating — checked against an app +// password's comma-separated scopes column so a password minted for "imap" +// can't be used to relay outbound SMTP, etc. +type Scope string + +const ( + ScopeSMTP Scope = "smtp" + ScopeIMAP Scope = "imap" + ScopePOP3 Scope = "pop3" + ScopeCalDAV Scope = "caldav" + ScopeCardDAV Scope = "carddav" +) + +// Authenticate verifies a username/password against either the user's main +// account password or one of their active, non-expired app passwords scoped +// for the given protocol. Returns the user and true on success. +func Authenticate(database *db.DB, username, password string, scope Scope) (*db.User, bool) { + user, err := database.LookupUserByEmail(strings.ToLower(strings.TrimSpace(username))) + if err != nil { + // Always compare against a dummy hash even on lookup failure — avoids + // leaking "user exists vs doesn't" via response timing. + bcrypt.CompareHashAndPassword([]byte(dummyHash), []byte(password)) + return nil, false + } + + if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) == nil { + return user, true + } + + if checkAppPassword(database, user.ID, password, scope) { + return user, true + } + + return nil, false +} + +func checkAppPassword(database *db.DB, userID, password string, scope Scope) bool { + rows, err := database.Query(` + SELECT id, password_hash, scopes, expires_at FROM app_passwords + WHERE user_id = ? AND (expires_at IS NULL OR expires_at > ?) + `, userID, time.Now().UTC()) + if err != nil { + slog.Error("app password lookup failed", "err", err) + return false + } + defer rows.Close() + + for rows.Next() { + var id, hash, scopes string + var expiresAt *time.Time + if err := rows.Scan(&id, &hash, &scopes, &expiresAt); err != nil { + continue + } + if !strings.Contains(scopes, string(scope)) && !strings.Contains(scopes, "all") { + continue + } + if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil { + go database.Exec(`UPDATE app_passwords SET last_used_at = ? WHERE id = ?`, time.Now().UTC(), id) + return true + } + } + return false +} + +// dummyHash is a valid bcrypt hash of a random unguessable string, used only +// to equalize timing when a username lookup fails. +const dummyHash = "$2a$12$gT3vXk8yZ1pQzM4nR7wS8eK9vL2mN5oP1qR3sT6uV8wX0yZ2aB4cD" diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..44090cc --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,308 @@ +// Package config loads gomail.yaml, applies environment variable overrides for +// secrets, and generates a documented example config on first run. +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Server ServerConfig `yaml:"server"` + TLS TLSConfig `yaml:"tls"` + Database DatabaseConfig `yaml:"database"` + Storage StorageConfig `yaml:"storage"` + RateLimits RateLimitConfig `yaml:"rate_limits"` + Pipeline PipelineConfig `yaml:"pipeline"` + Notify NotifyConfig `yaml:"notify"` + POP3 POP3Config `yaml:"pop3"` + JMAP JMAPConfig `yaml:"jmap"` + OAuth OAuthConfig `yaml:"oauth"` + LinkedAccounts LinkedAccountsConfig `yaml:"linked_accounts"` + Security SecurityConfig `yaml:"-"` // populated entirely from env, never serialized +} + +type ServerConfig struct { + Hostname string `yaml:"hostname"` + SMTPAddr string `yaml:"smtp_addr"` + SubmissionAddr string `yaml:"submission_addr"` + SMTPSAddr string `yaml:"smtps_addr"` + IMAPAddr string `yaml:"imap_addr"` + IMAPSAddr string `yaml:"imaps_addr"` + WebmailAddr string `yaml:"webmail_addr"` + AdminAddr string `yaml:"admin_addr"` + DAVAddr string `yaml:"dav_addr"` + ManageSieveAddr string `yaml:"managesieve_addr"` + RealIPHeader string `yaml:"real_ip_header"` + AdminIPAllowlist []string `yaml:"admin_ip_allowlist"` +} + +type TLSConfig struct { + Mode string `yaml:"mode"` // acme | file | off + ACMEEmail string `yaml:"acme_email"` + ACMEDomains []string `yaml:"acme_domains"` + ACMEDirectoryURL string `yaml:"acme_directory_url"` // defaults to real Let's Encrypt production; override for staging/testing + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` + MinVersion string `yaml:"min_version"` +} + +type DatabaseConfig struct { + Driver string `yaml:"driver"` // sqlite | postgres | mysql + DSN string `yaml:"dsn"` +} + +type StorageConfig struct { + MaildirRoot string `yaml:"maildir_root"` + RetentionDays int `yaml:"retention_days"` + QuarantineDays int `yaml:"quarantine_days"` + MaxMessageSizeMB int `yaml:"max_message_size_mb"` +} + +type RateLimitConfig struct { + SMTPConnPerMin int `yaml:"smtp_conn_per_min"` + SMTPAuthFailures int `yaml:"smtp_auth_failures"` + IMAPConnPerMin int `yaml:"imap_conn_per_min"` + IMAPAuthFailures int `yaml:"imap_auth_failures"` + POP3AuthFailures int `yaml:"pop3_auth_failures"` + HTTPReqPerMin int `yaml:"http_req_per_min"` +} + +type PipelineConfig struct { + ScoreFlag float64 `yaml:"score_flag"` + ScoreQuarantine float64 `yaml:"score_quarantine"` + ScoreBlock float64 `yaml:"score_block"` + ClamAVSocket string `yaml:"clamav_socket"` + RspamdURL string `yaml:"rspamd_url"` + LLMURL string `yaml:"llm_url"` + LLMModel string `yaml:"llm_model"` + LLMTimeoutSecs int `yaml:"llm_timeout_secs"` +} + +type NotifyConfig struct { + SMTPHost string `yaml:"smtp_host"` + SMTPPort int `yaml:"smtp_port"` + SMTPUser string `yaml:"smtp_user"` + FromAddress string `yaml:"from_address"` + DefaultDigestIntervalMins int `yaml:"default_digest_interval_mins"` +} + +type POP3Config struct { + Enabled bool `yaml:"enabled"` // off by default — legacy, opt-in + POP3Addr string `yaml:"pop3_addr"` + POP3SAddr string `yaml:"pop3s_addr"` +} + +type JMAPConfig struct { + ExternalEnabled bool `yaml:"external_enabled"` // off by default + ExternalAddr string `yaml:"external_addr"` +} + +type OAuthConfig struct { + Google OAuthProviderConfig `yaml:"google"` + Microsoft OAuthProviderConfig `yaml:"microsoft"` +} + +type OAuthProviderConfig struct { + Enabled bool `yaml:"enabled"` + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret,omitempty"` // prefer env override + Tenant string `yaml:"tenant,omitempty"` // microsoft only + RedirectURI string `yaml:"redirect_uri"` +} + +type LinkedAccountsConfig struct { + DefaultCacheRetention string `yaml:"default_cache_retention"` // e.g. "90d" + MaxCacheRetention string `yaml:"max_cache_retention"` // e.g. "3y" + CacheSweepInterval string `yaml:"cache_sweep_interval"` // e.g. "24h" + SyncPollIntervalSecs int `yaml:"sync_poll_interval_secs"` +} + +// SecurityConfig holds every secret. Populated ONLY from environment variables — +// never read from or written to the YAML config file. +type SecurityConfig struct { + MasterKey string // GOMAIL_MASTER_KEY (32-byte hex) + MasterKeyPrev string // GOMAIL_MASTER_KEY_PREV (during rotation) + JWTSecret string // GOMAIL_JWT_SECRET + AdminInitPassword string // GOMAIL_ADMIN_INIT_PASSWORD + NotifySMTPPassword string // GOMAIL_NOTIFY_SMTP_PASSWORD + OAuthGoogleSecret string // GOMAIL_OAUTH_GOOGLE_SECRET + OAuthMicrosoftSecret string // GOMAIL_OAUTH_MICROSOFT_SECRET + DBDSNOverride string // GOMAIL_DB_DSN + BcryptCost int // GOMAIL_BCRYPT_COST (default 12) +} + +// Load reads the YAML config at path, auto-generating a default one if it does +// not exist, then applies environment variable overrides for all secrets. +func Load(path string) (*Config, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := writeDefault(path); err != nil { + return nil, fmt.Errorf("generating default config: %w", err) + } + fmt.Printf("No config found — generated default at %s. Review it before production use.\n", path) + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading config: %w", err) + } + + cfg := Default() + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, fmt.Errorf("parsing config: %w", err) + } + + applyEnvOverrides(cfg) + + if err := validate(cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +func applyEnvOverrides(cfg *Config) { + cfg.Security = SecurityConfig{ + MasterKey: os.Getenv("GOMAIL_MASTER_KEY"), + MasterKeyPrev: os.Getenv("GOMAIL_MASTER_KEY_PREV"), + JWTSecret: os.Getenv("GOMAIL_JWT_SECRET"), + AdminInitPassword: os.Getenv("GOMAIL_ADMIN_INIT_PASSWORD"), + NotifySMTPPassword: os.Getenv("GOMAIL_NOTIFY_SMTP_PASSWORD"), + OAuthGoogleSecret: os.Getenv("GOMAIL_OAUTH_GOOGLE_SECRET"), + OAuthMicrosoftSecret: os.Getenv("GOMAIL_OAUTH_MICROSOFT_SECRET"), + DBDSNOverride: os.Getenv("GOMAIL_DB_DSN"), + BcryptCost: 12, + } + + if cfg.Security.DBDSNOverride != "" { + cfg.Database.DSN = cfg.Security.DBDSNOverride + } + if cfg.Security.OAuthGoogleSecret != "" { + cfg.OAuth.Google.ClientSecret = cfg.Security.OAuthGoogleSecret + } + if cfg.Security.OAuthMicrosoftSecret != "" { + cfg.OAuth.Microsoft.ClientSecret = cfg.Security.OAuthMicrosoftSecret + } +} + +func validate(cfg *Config) error { + if cfg.Security.MasterKey == "" { + return fmt.Errorf("GOMAIL_MASTER_KEY environment variable is required (32-byte hex — generate with: openssl rand -hex 32)") + } + if len(cfg.Security.MasterKey) != 64 { + return fmt.Errorf("GOMAIL_MASTER_KEY must be 64 hex characters (32 bytes), got %d characters", len(cfg.Security.MasterKey)) + } + if cfg.Security.JWTSecret == "" { + return fmt.Errorf("GOMAIL_JWT_SECRET environment variable is required (generate with: openssl rand -hex 32)") + } + if len(cfg.Security.JWTSecret) < 32 { + return fmt.Errorf("GOMAIL_JWT_SECRET must be at least 32 characters, got %d (generate with: openssl rand -hex 32)", len(cfg.Security.JWTSecret)) + } + if cfg.Server.Hostname == "" { + return fmt.Errorf("server.hostname must be set in config") + } + return nil +} + +// Default returns a Config populated with sane defaults (used as the base +// before YAML unmarshal, so any keys missing from the file keep these values). +func Default() *Config { + return &Config{ + Server: ServerConfig{ + Hostname: "mail.example.com", + SMTPAddr: ":25", + SubmissionAddr: ":587", + SMTPSAddr: ":465", + IMAPAddr: ":143", + IMAPSAddr: ":993", + WebmailAddr: "127.0.0.1:8080", + AdminAddr: "127.0.0.1:9090", + DAVAddr: "127.0.0.1:8443", + ManageSieveAddr: ":4190", + RealIPHeader: "X-Forwarded-For", + AdminIPAllowlist: []string{"127.0.0.1", "::1"}, + }, + TLS: TLSConfig{ + Mode: "acme", + ACMEDirectoryURL: "https://acme-v02.api.letsencrypt.org/directory", + MinVersion: "TLS12", + }, + Database: DatabaseConfig{ + Driver: "sqlite", + DSN: "file:/var/lib/gomail/gomail.db?_journal_mode=WAL&_foreign_keys=on", + }, + Storage: StorageConfig{ + MaildirRoot: "/var/mail/gomail", + RetentionDays: 365, + QuarantineDays: 30, + MaxMessageSizeMB: 50, + }, + RateLimits: RateLimitConfig{ + SMTPConnPerMin: 20, + SMTPAuthFailures: 5, + IMAPConnPerMin: 60, + IMAPAuthFailures: 5, + POP3AuthFailures: 5, + HTTPReqPerMin: 120, + }, + Pipeline: PipelineConfig{ + ScoreFlag: 20, + ScoreQuarantine: 50, + ScoreBlock: 80, + LLMModel: "llama3.2-3b-instruct", + LLMTimeoutSecs: 30, + }, + Notify: NotifyConfig{ + SMTPPort: 587, + FromAddress: "noreply@example.com", + DefaultDigestIntervalMins: 60, + }, + POP3: POP3Config{ + Enabled: false, + POP3Addr: ":110", + POP3SAddr: ":995", + }, + JMAP: JMAPConfig{ + ExternalEnabled: false, + ExternalAddr: "0.0.0.0:8443", + }, + OAuth: OAuthConfig{ + Google: OAuthProviderConfig{Enabled: false}, + Microsoft: OAuthProviderConfig{Enabled: false, Tenant: "common"}, + }, + LinkedAccounts: LinkedAccountsConfig{ + DefaultCacheRetention: "90d", + MaxCacheRetention: "3y", + CacheSweepInterval: "24h", + SyncPollIntervalSecs: 120, + }, + } +} + +func writeDefault(path string) error { + cfg := Default() + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + + header := `# gomail.yaml — auto-generated. Review before production use. +# +# Secrets are NOT stored here — set these environment variables instead: +# GOMAIL_MASTER_KEY 32-byte hex, message/contact/calendar encryption key +# generate with: openssl rand -hex 32 +# GOMAIL_JWT_SECRET 32+ byte random, session signing +# generate with: openssl rand -hex 32 +# GOMAIL_ADMIN_INIT_PASSWORD first-run global admin password +# GOMAIL_DB_DSN overrides database.dsn below +# GOMAIL_NOTIFY_SMTP_PASSWORD outbound notification SMTP password +# GOMAIL_OAUTH_GOOGLE_SECRET Google OAuth2 client secret +# GOMAIL_OAUTH_MICROSOFT_SECRET Microsoft OAuth2 client secret +# GOMAIL_MASTER_KEY_PREV previous master key, only during key rotation + +` + full := append([]byte(header), data...) + return os.WriteFile(path, full, 0640) +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..9330518 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,166 @@ +// Package crypto provides encryption-at-rest for messages, contacts, and +// calendar data. Every record gets its own key, derived from the master key +// via HKDF — compromising one encrypted file never exposes the master key or +// any other record. +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +const ( + keySize = 32 // AES-256 + nonceSize = 12 // GCM standard nonce size +) + +// MasterKey holds the decoded master key(s) used to derive per-record keys. +// Two keys allow decrypting old data during a key-rotation window: Current is +// used for new writes, Previous (if set) is tried as a fallback on decrypt. +type MasterKey struct { + Current []byte + Previous []byte // nil if not rotating +} + +// LoadMasterKey decodes hex-encoded master key(s) from config/env values. +func LoadMasterKey(currentHex, previousHex string) (*MasterKey, error) { + cur, err := decodeKey(currentHex) + if err != nil { + return nil, fmt.Errorf("master key: %w", err) + } + mk := &MasterKey{Current: cur} + if previousHex != "" { + prev, err := decodeKey(previousHex) + if err != nil { + return nil, fmt.Errorf("previous master key: %w", err) + } + mk.Previous = prev + } + return mk, nil +} + +func decodeKey(hexStr string) ([]byte, error) { + b, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("invalid hex: %w", err) + } + if len(b) != keySize { + return nil, fmt.Errorf("must be %d bytes (%d hex chars), got %d bytes", keySize, keySize*2, len(b)) + } + return b, nil +} + +// deriveKey produces a per-record 32-byte key from the master key using HKDF-SHA256. +// recordID should be a stable, unique identifier for the record (e.g. message ID, +// contact UID) — using the same recordID always derives the same key, which is +// required for decryption to work. +func deriveKey(master []byte, recordID string, purpose string) ([]byte, error) { + info := []byte(purpose + ":" + recordID) + r := hkdf.New(sha256.New, master, nil, info) + key := make([]byte, keySize) + if _, err := io.ReadFull(r, key); err != nil { + return nil, fmt.Errorf("hkdf derive: %w", err) + } + return key, nil +} + +// Encrypt encrypts plaintext with a key derived from the master key and +// recordID. purpose namespaces the derivation (e.g. "message", "contact", +// "calendar", "dkim-key") so the same recordID used for different data types +// never collides. Output format: [12-byte nonce][ciphertext][16-byte GCM tag]. +func Encrypt(mk *MasterKey, recordID, purpose string, plaintext []byte) ([]byte, error) { + key, err := deriveKey(mk.Current, recordID, purpose) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("aes cipher: %w", err) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("gcm: %w", err) + } + + nonce := make([]byte, nonceSize) + if _, err := rand.Read(nonce); err != nil { + return nil, fmt.Errorf("nonce: %w", err) + } + + ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + return append(nonce, ciphertext...), nil +} + +// Decrypt decrypts data produced by Encrypt. It tries the current master key +// first, then falls back to the previous key (if set) — this lets records +// written before a key rotation still be read without a bulk re-encryption +// pass; callers should re-encrypt with the current key on next write if a +// fallback decrypt succeeds. +func Decrypt(mk *MasterKey, recordID, purpose string, data []byte) ([]byte, error) { + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + + if pt, err := decryptWith(mk.Current, recordID, purpose, nonce, ciphertext); err == nil { + return pt, nil + } + + if mk.Previous != nil { + if pt, err := decryptWith(mk.Previous, recordID, purpose, nonce, ciphertext); err == nil { + return pt, nil + } + } + + return nil, fmt.Errorf("decrypt failed with current%s key", + map[bool]string{true: " and previous", false: ""}[mk.Previous != nil]) +} + +func decryptWith(master []byte, recordID, purpose string, nonce, ciphertext []byte) ([]byte, error) { + key, err := deriveKey(master, recordID, purpose) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + return gcm.Open(nil, nonce, ciphertext, nil) +} + +// NeedsReencryption reports whether data was decrypted using the previous +// (not current) master key — callers use this to trigger lazy re-encryption +// on read during a key rotation window. +func NeedsReencryption(mk *MasterKey, recordID, purpose string, data []byte) bool { + if mk.Previous == nil || len(data) < nonceSize { + return false + } + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + if _, err := decryptWith(mk.Current, recordID, purpose, nonce, ciphertext); err == nil { + return false // current key works fine + } + _, err := decryptWith(mk.Previous, recordID, purpose, nonce, ciphertext) + return err == nil +} + +// GenerateMasterKeyHex is a convenience helper for CLI tooling / setup docs — +// produces a fresh random master key as hex, ready to paste into GOMAIL_MASTER_KEY. +func GenerateMasterKeyHex() (string, error) { + b := make([]byte, keySize) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/internal/dav/dav.go b/internal/dav/dav.go new file mode 100644 index 0000000..ad14e23 --- /dev/null +++ b/internal/dav/dav.go @@ -0,0 +1,471 @@ +// Package dav implements a CalDAV (RFC 4791) + CardDAV (RFC 6352) HTTP +// server covering the core operations real clients need: PROPFIND (Depth 0/1 +// discovery), REPORT (calendar-query/multiget, addressbook-query/multiget — +// query filtering returns all objects in the collection in this pass, full +// time-range/property filtering deferred), PUT (create/update), GET +// (fetch), DELETE, OPTIONS. No MKCALENDAR/MKCOL — every user's default +// addressbook and calendar are auto-created on first access instead, which +// covers the common case (one addressbook, one calendar per user) without +// needing collection-creation UI in an early phase. +// +// URL layout: +// +// /dav/contacts/{ownerType}/{ownerID}/ addressbook collection +// /dav/contacts/{ownerType}/{ownerID}/{uid}.vcf a contact +// /dav/calendars/{ownerType}/{ownerID}/ calendar collection +// /dav/calendars/{ownerType}/{ownerID}/{uid}.ics a calendar event +package dav + +import ( + "encoding/xml" + "fmt" + "io" + "log/slog" + "net/http" + "strings" + "time" + + "gomail/internal/auth" + "gomail/internal/crypto" + "gomail/internal/db" + "gomail/internal/ical" + "gomail/internal/vcard" + "github.com/google/uuid" +) + +type Handler struct { + database *db.DB + mk *crypto.MasterKey +} + +func NewHandler(database *db.DB, mk *crypto.MasterKey) *Handler { + return &Handler{database: database, mk: mk} +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + user, ok := h.authenticate(r) + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="GoMail DAV"`) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + path := strings.TrimPrefix(r.URL.Path, "/dav") + switch { + case strings.HasPrefix(path, "/contacts/"): + h.serveCardDAV(w, r, user, strings.TrimPrefix(path, "/contacts/")) + case strings.HasPrefix(path, "/calendars/"): + h.serveCalDAV(w, r, user, strings.TrimPrefix(path, "/calendars/")) + default: + http.NotFound(w, r) + } +} + +func (h *Handler) authenticate(r *http.Request) (*db.User, bool) { + username, password, ok := r.BasicAuth() + if !ok { + return nil, false + } + return auth.Authenticate(h.database, username, password, auth.ScopeCardDAV) +} + +// ── CardDAV ─────────────────────────────────────────────────────────────────── + +func (h *Handler) serveCardDAV(w http.ResponseWriter, r *http.Request, user *db.User, rest string) { + ownerType, ownerID, uid, ok := parseCollectionPath(rest, user) + if !ok { + http.NotFound(w, r) + return + } + + book, err := h.database.GetOrCreateAddressbook(ownerType, ownerID, "Default") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch r.Method { + case "OPTIONS": + w.Header().Set("DAV", "1, 2, addressbook") + w.Header().Set("Allow", "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE") + w.WriteHeader(http.StatusOK) + + case "PROPFIND": + h.propfindContacts(w, r, book, uid) + + case "REPORT": + h.reportContacts(w, r, book) + + case http.MethodGet: + if uid == "" { + http.Error(w, "GET on collection not supported, use PROPFIND", http.StatusMethodNotAllowed) + return + } + contact, err := h.database.GetContact(book.ID, strings.TrimSuffix(uid, ".vcf")) + if err != nil { + http.NotFound(w, r) + return + } + plain, err := crypto.Decrypt(h.mk, contact.ID, "contact", contact.VCardEnc) + if err != nil { + http.Error(w, "decrypt error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/vcard; charset=utf-8") + w.Header().Set("ETag", contact.ETag) + w.Write(plain) + + case http.MethodPut: + if uid == "" { + http.Error(w, "PUT requires a resource path", http.StatusBadRequest) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read error", http.StatusBadRequest) + return + } + card, err := vcard.Parse(string(body)) + if err != nil { + http.Error(w, "invalid vCard: "+err.Error(), http.StatusBadRequest) + return + } + contactID := uuid.NewString() + if existing, err := h.database.GetContact(book.ID, card.UID); err == nil { + contactID = existing.ID + } + encrypted, err := crypto.Encrypt(h.mk, contactID, "contact", body) + if err != nil { + http.Error(w, "encrypt error", http.StatusInternalServerError) + return + } + etag := fmt.Sprintf(`"%d"`, time.Now().UnixNano()) + if err := h.database.UpsertContact(&db.Contact{ + ID: contactID, AddressbookID: book.ID, UID: card.UID, VCardEnc: encrypted, ETag: etag, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("ETag", etag) + w.WriteHeader(http.StatusCreated) + + case http.MethodDelete: + if uid == "" { + http.Error(w, "DELETE requires a resource path", http.StatusBadRequest) + return + } + if err := h.database.DeleteContact(book.ID, strings.TrimSuffix(uid, ".vcf")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (h *Handler) propfindContacts(w http.ResponseWriter, r *http.Request, book *db.Addressbook, uid string) { + depth := r.Header.Get("Depth") + + var responses []multistatusResponse + responses = append(responses, multistatusResponse{ + Href: r.URL.Path, + Props: propSet{ + DisplayName: book.DisplayName, + ResourceType: "", + }, + }) + + if depth == "1" && uid == "" { + contacts, err := h.database.ListContacts(book.ID) + if err == nil { + for _, c := range contacts { + responses = append(responses, multistatusResponse{ + Href: strings.TrimSuffix(r.URL.Path, "/") + "/" + c.UID + ".vcf", + Props: propSet{ETag: c.ETag, ContentType: "text/vcard; charset=utf-8"}, + }) + } + } + } + + writeMultistatus(w, responses) +} + +func (h *Handler) reportContacts(w http.ResponseWriter, r *http.Request, book *db.Addressbook) { + // addressbook-query and addressbook-multiget both return every contact's + // current vCard in this pass — full filter/prop-match parsing is + // deferred; clients doing a multiget for hrefs they already have (the + // common sync pattern) get correct data, just not a filtered subset. + contacts, err := h.database.ListContacts(book.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var responses []multistatusResponse + for _, c := range contacts { + plain, err := crypto.Decrypt(h.mk, c.ID, "contact", c.VCardEnc) + if err != nil { + continue + } + responses = append(responses, multistatusResponse{ + Href: strings.TrimSuffix(r.URL.Path, "/") + "/" + c.UID + ".vcf", + Props: propSet{ETag: c.ETag}, + AddressData: string(plain), + }) + } + writeMultistatus(w, responses) +} + +// ── CalDAV ──────────────────────────────────────────────────────────────────── + +func (h *Handler) serveCalDAV(w http.ResponseWriter, r *http.Request, user *db.User, rest string) { + ownerType, ownerID, uid, ok := parseCollectionPath(rest, user) + if !ok { + http.NotFound(w, r) + return + } + + cal, err := h.database.GetOrCreateCalendar(ownerType, ownerID, "Default") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch r.Method { + case "OPTIONS": + w.Header().Set("DAV", "1, 2, calendar-access") + w.Header().Set("Allow", "OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE") + w.WriteHeader(http.StatusOK) + + case "PROPFIND": + h.propfindCalendar(w, r, cal, uid) + + case "REPORT": + h.reportCalendar(w, r, cal) + + case http.MethodGet: + if uid == "" { + http.Error(w, "GET on collection not supported, use PROPFIND", http.StatusMethodNotAllowed) + return + } + obj, err := h.database.GetCalendarObject(cal.ID, strings.TrimSuffix(uid, ".ics")) + if err != nil { + http.NotFound(w, r) + return + } + plain, err := crypto.Decrypt(h.mk, obj.ID, "calendar", obj.ICalEnc) + if err != nil { + http.Error(w, "decrypt error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/calendar; charset=utf-8") + w.Header().Set("ETag", obj.ETag) + w.Write(plain) + + case http.MethodPut: + if uid == "" { + http.Error(w, "PUT requires a resource path", http.StatusBadRequest) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read error", http.StatusBadRequest) + return + } + event, err := ical.Parse(string(body)) + if err != nil { + http.Error(w, "invalid iCal: "+err.Error(), http.StatusBadRequest) + return + } + objID := uuid.NewString() + if existing, err := h.database.GetCalendarObject(cal.ID, event.UID); err == nil { + objID = existing.ID + } + encrypted, err := crypto.Encrypt(h.mk, objID, "calendar", body) + if err != nil { + http.Error(w, "encrypt error", http.StatusInternalServerError) + return + } + etag := fmt.Sprintf(`"%d"`, time.Now().UnixNano()) + obj := &db.CalendarObject{ + ID: objID, CalendarID: cal.ID, UID: event.UID, ICalEnc: encrypted, + ComponentType: "VEVENT", Summary: event.Summary, ETag: etag, + } + if !event.DTStart.IsZero() { + obj.DTStart = &event.DTStart + } + if !event.DTEnd.IsZero() { + obj.DTEnd = &event.DTEnd + } + if err := h.database.UpsertCalendarObject(obj); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("ETag", etag) + w.WriteHeader(http.StatusCreated) + + case http.MethodDelete: + if uid == "" { + http.Error(w, "DELETE requires a resource path", http.StatusBadRequest) + return + } + if err := h.database.DeleteCalendarObject(cal.ID, strings.TrimSuffix(uid, ".ics")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (h *Handler) propfindCalendar(w http.ResponseWriter, r *http.Request, cal *db.Calendar, uid string) { + depth := r.Header.Get("Depth") + + var responses []multistatusResponse + responses = append(responses, multistatusResponse{ + Href: r.URL.Path, + Props: propSet{ + DisplayName: cal.DisplayName, + ResourceType: "", + }, + }) + + if depth == "1" && uid == "" { + objs, err := h.database.ListCalendarObjects(cal.ID) + if err == nil { + for _, o := range objs { + responses = append(responses, multistatusResponse{ + Href: strings.TrimSuffix(r.URL.Path, "/") + "/" + o.UID + ".ics", + Props: propSet{ETag: o.ETag, ContentType: "text/calendar; charset=utf-8"}, + }) + } + } + } + + writeMultistatus(w, responses) +} + +func (h *Handler) reportCalendar(w http.ResponseWriter, r *http.Request, cal *db.Calendar) { + // calendar-query and calendar-multiget both return every event in this + // pass — time-range filtering (the most common real-world calendar-query + // use, "give me events this week") is deferred; noted here rather than + // silently ignored, since clients that rely on server-side time-range + // filtering will over-fetch until that lands. + objs, err := h.database.ListCalendarObjects(cal.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var responses []multistatusResponse + for _, o := range objs { + plain, err := crypto.Decrypt(h.mk, o.ID, "calendar", o.ICalEnc) + if err != nil { + continue + } + responses = append(responses, multistatusResponse{ + Href: strings.TrimSuffix(r.URL.Path, "/") + "/" + o.UID + ".ics", + Props: propSet{ETag: o.ETag}, + CalendarData: string(plain), + }) + } + writeMultistatus(w, responses) +} + +// ── Path parsing ────────────────────────────────────────────────────────────── + +// parseCollectionPath extracts (ownerType, ownerID, resourceUID) from a +// request path like "user/{userID}/{uid}.vcf" or "tenant/{tenantID}/". Only +// allows a user to address their own personal collection or their own +// tenant's shared one — cross-user access is rejected. +func parseCollectionPath(rest string, requestingUser *db.User) (db.OwnerType, string, string, bool) { + parts := strings.SplitN(strings.TrimPrefix(rest, "/"), "/", 3) + if len(parts) < 2 { + return "", "", "", false + } + ownerType := db.OwnerType(parts[0]) + ownerID := parts[1] + uid := "" + if len(parts) == 3 { + uid = parts[2] + } + + switch ownerType { + case db.OwnerUser: + if ownerID != requestingUser.ID { + return "", "", "", false // no cross-user access + } + case db.OwnerTenant: + if ownerID != requestingUser.TenantID { + return "", "", "", false // no cross-tenant access + } + default: + return "", "", "", false + } + return ownerType, ownerID, uid, true +} + +// ── Multistatus XML ─────────────────────────────────────────────────────────── + +type propSet struct { + DisplayName string + ResourceType string // raw XML fragment, since it varies by collection type + ETag string + ContentType string +} + +type multistatusResponse struct { + Href string + Props propSet + AddressData string // set only for CardDAV REPORT responses + CalendarData string // set only for CalDAV REPORT responses +} + +func writeMultistatus(w http.ResponseWriter, responses []multistatusResponse) { + var b strings.Builder + b.WriteString(xml.Header) + b.WriteString(`` + "\n") + + for _, r := range responses { + b.WriteString(" \n") + b.WriteString(" " + xmlEscape(r.Href) + "\n") + b.WriteString(" \n \n") + if r.Props.DisplayName != "" { + b.WriteString(" " + xmlEscape(r.Props.DisplayName) + "\n") + } + if r.Props.ResourceType != "" { + b.WriteString(" " + r.Props.ResourceType + "\n") + } + if r.Props.ETag != "" { + b.WriteString(" " + xmlEscape(r.Props.ETag) + "\n") + } + if r.Props.ContentType != "" { + b.WriteString(" " + xmlEscape(r.Props.ContentType) + "\n") + } + if r.AddressData != "" { + b.WriteString(" " + xmlEscape(r.AddressData) + "\n") + } + if r.CalendarData != "" { + b.WriteString(" " + xmlEscape(r.CalendarData) + "\n") + } + b.WriteString(" \n HTTP/1.1 200 OK\n \n") + b.WriteString(" \n") + } + b.WriteString("\n") + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.WriteHeader(207) // Multi-Status + if _, err := w.Write([]byte(b.String())); err != nil { + slog.Debug("dav: write error", "err", err) + } +} + +func xmlEscape(s string) string { + var b strings.Builder + xml.EscapeText(&b, []byte(s)) + return b.String() +} diff --git a/internal/db/bootstrap.go b/internal/db/bootstrap.go new file mode 100644 index 0000000..b0cf765 --- /dev/null +++ b/internal/db/bootstrap.go @@ -0,0 +1,96 @@ +package db + +import ( + "fmt" + "log/slog" + + "gomail/internal/crypto" + "gomail/internal/dkim" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +// Bootstrap creates an initial tenant, domain, and global admin user on first +// run — detected by the absence of any global_admin row. Safe to call on +// every startup; it's a no-op once bootstrapped. +func (db *DB) Bootstrap(hostname, initPassword string, bcryptCost int, mk *crypto.MasterKey) error { + var count int + err := db.QueryRow("SELECT COUNT(*) FROM users WHERE role = 'global_admin'").Scan(&count) + if err != nil { + return fmt.Errorf("checking existing admins: %w", err) + } + if count > 0 { + return nil // already bootstrapped + } + + if initPassword == "" { + initPassword = "ChangeMe123!" + slog.Warn("no admin exists and GOMAIL_ADMIN_INIT_PASSWORD not set — using default, CHANGE IMMEDIATELY", + "password", initPassword, "email", "admin@"+hostname) + } + + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + tenantID := uuid.NewString() + if _, err := tx.Exec( + `INSERT INTO tenants (id, name, display_name) VALUES (?, ?, ?)`, + tenantID, "default", "Default Tenant", + ); err != nil { + return fmt.Errorf("creating default tenant: %w", err) + } + + domainID := uuid.NewString() + dkimSelector := "mail" + + // Generate a DKIM key pair so outbound mail can be signed immediately — + // without this, every message this instance sends would be unsigned + // until an admin manually configures one (Phase 8's admin portal will + // add key rotation/regeneration; this just ensures a working default). + var dkimKeyEnc []byte + kp, kpErr := dkim.GenerateKeyPair() + if kpErr != nil { + slog.Warn("failed to generate DKIM key during bootstrap — outbound mail will be unsigned until one is configured", "err", kpErr) + } else { + encrypted, encErr := crypto.Encrypt(mk, domainID, "dkim-key", kp.PrivateKeyPEM) + if encErr != nil { + slog.Warn("failed to encrypt DKIM key during bootstrap", "err", encErr) + } else { + dkimKeyEnc = encrypted + slog.Info("DKIM key generated for default domain — publish this DNS TXT record", + "record_name", dkimSelector+"._domainkey."+hostname, + "record_value", kp.DNSRecordValue) + } + } + + if _, err := tx.Exec( + `INSERT INTO domains (id, tenant_id, domain, active, accept_all, dkim_selector, dkim_private_key_enc) VALUES (?, ?, ?, 1, 1, ?, ?)`, + domainID, tenantID, hostname, dkimSelector, dkimKeyEnc, + ); err != nil { + return fmt.Errorf("creating default domain: %w", err) + } + + hash, err := bcrypt.GenerateFromPassword([]byte(initPassword), bcryptCost) + if err != nil { + return fmt.Errorf("hashing admin password: %w", err) + } + + adminEmail := "admin@" + hostname + if _, err := tx.Exec( + `INSERT INTO users (id, tenant_id, domain_id, email, password_hash, display_name, role, active) + VALUES (?, ?, ?, ?, ?, ?, 'global_admin', 1)`, + uuid.NewString(), tenantID, domainID, adminEmail, string(hash), "Global Admin", + ); err != nil { + return fmt.Errorf("creating admin user: %w", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("committing bootstrap: %w", err) + } + + slog.Info("bootstrap complete", "admin_email", adminEmail, "tenant", "default", "domain", hostname) + return nil +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..7bcff08 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,158 @@ +// Package db wraps database/sql with the gomail schema. No ORM — raw SQL with +// prepared statements only, per the project's minimal-dependency principle. +package db + +import ( + "database/sql" + "fmt" + "log/slog" + "strings" + + _ "github.com/mattn/go-sqlite3" +) + +// DB wraps *sql.DB with the driver name (some queries need driver-specific SQL, +// e.g. placeholder syntax differs between sqlite/postgres/mysql). +type DB struct { + *sql.DB + Driver string +} + +// Open connects to the database using the configured driver. +// SQLite is always available; postgres and mysql require build tags: +// +// go build -tags postgres . +// go build -tags mysql . +func Open(driver, dsn string) (*DB, error) { + d := strings.ToLower(driver) + + var sqlDriverName string + switch d { + case "sqlite", "": + sqlDriverName = "sqlite3" + d = "sqlite" + default: + name, ok := driverRegistry[d] + if !ok { + available := []string{"sqlite"} + for k := range driverRegistry { + available = append(available, k) + } + return nil, fmt.Errorf("driver %q not compiled in; rebuild with -tags %s. Available: %v", driver, driver, available) + } + sqlDriverName = name + } + + sqlDB, err := sql.Open(sqlDriverName, dsn) + if err != nil { + return nil, fmt.Errorf("opening database: %w", err) + } + + if d == "sqlite" { + // SQLite doesn't handle concurrent writers well — serialize via single conn. + sqlDB.SetMaxOpenConns(1) + if _, err := sqlDB.Exec("PRAGMA journal_mode=WAL"); err != nil { + return nil, fmt.Errorf("enabling WAL mode: %w", err) + } + if _, err := sqlDB.Exec("PRAGMA foreign_keys=ON"); err != nil { + return nil, fmt.Errorf("enabling foreign keys: %w", err) + } + if _, err := sqlDB.Exec("PRAGMA busy_timeout=5000"); err != nil { + return nil, fmt.Errorf("setting busy timeout: %w", err) + } + } + + if err := sqlDB.Ping(); err != nil { + return nil, fmt.Errorf("ping database: %w", err) + } + + return &DB{DB: sqlDB, Driver: d}, nil +} + +// driverRegistry is populated by build-tag-gated driver_*.go files +// (driver_postgres.go, driver_mysql.go) via init(). +var driverRegistry = map[string]string{} + +func registerDriver(name, sqlDriverName string) { + driverRegistry[strings.ToLower(name)] = sqlDriverName +} + +// Migrate runs all pending schema migrations in order. Migrations are +// idempotent (CREATE TABLE IF NOT EXISTS) so this is always safe to call at +// startup. +func (db *DB) Migrate() error { + slog.Info("running database migrations") + + if _, err := db.Exec(migrationsTableSQL[db.Driver]); err != nil { + return fmt.Errorf("creating migrations table: %w", err) + } + + for _, m := range migrations { + applied, err := db.migrationApplied(m.name) + if err != nil { + return fmt.Errorf("checking migration %s: %w", m.name, err) + } + if applied { + continue + } + + stmt := m.sql[db.Driver] + if stmt == "" { + stmt = m.sql["sqlite"] // fall back — most DDL is portable enough via driver quirks handled per-migration + } + + slog.Info("applying migration", "name", m.name) + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("begin tx for %s: %w", m.name, err) + } + if _, err := tx.Exec(stmt); err != nil { + tx.Rollback() + return fmt.Errorf("applying migration %s: %w", m.name, err) + } + if _, err := tx.Exec(db.insertMigrationSQL(), m.name); err != nil { + tx.Rollback() + return fmt.Errorf("recording migration %s: %w", m.name, err) + } + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit migration %s: %w", m.name, err) + } + } + + slog.Info("migrations complete", "count", len(migrations)) + return nil +} + +func (db *DB) migrationApplied(name string) (bool, error) { + var count int + err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE name = "+db.placeholder(1), name).Scan(&count) + return count > 0, err +} + +func (db *DB) insertMigrationSQL() string { + return "INSERT INTO schema_migrations (name, applied_at) VALUES (" + db.placeholder(1) + ", CURRENT_TIMESTAMP)" +} + +// placeholder returns the driver-appropriate positional parameter syntax. +// sqlite/mysql use "?", postgres uses "$1", "$2", ... +func (db *DB) placeholder(n int) string { + if db.Driver == "postgres" { + return fmt.Sprintf("$%d", n) + } + return "?" +} + +var migrationsTableSQL = map[string]string{ + "sqlite": `CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT PRIMARY KEY, + applied_at DATETIME NOT NULL + )`, + "postgres": `CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL + )`, + "mysql": `CREATE TABLE IF NOT EXISTS schema_migrations ( + name VARCHAR(255) PRIMARY KEY, + applied_at DATETIME NOT NULL + )`, +} diff --git a/internal/db/migrations.go b/internal/db/migrations.go new file mode 100644 index 0000000..f7ba354 --- /dev/null +++ b/internal/db/migrations.go @@ -0,0 +1,374 @@ +package db + +// migration is a single named schema change with driver-specific SQL variants. +// SQLite is the reference dialect (required); postgres/mysql variants are +// filled in as those build-tagged drivers are added — until then the sqlite +// SQL is close enough to run in most cases (TEXT/BLOB/DATETIME map cleanly). +type migration struct { + name string + sql map[string]string +} + +var migrations = []migration{ + { + name: "0001_tenants_domains", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE tenants ( + id TEXT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + display_name TEXT, + digest_interval_mins INTEGER NOT NULL DEFAULT 60, + max_accounts INTEGER NOT NULL DEFAULT 0, + quota_mb_per_user INTEGER NOT NULL DEFAULT 2048, + settings_json TEXT NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE domains ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + domain TEXT UNIQUE NOT NULL, + active INTEGER NOT NULL DEFAULT 1, + dkim_selector TEXT, + dkim_private_key_enc BLOB, + accept_all INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_domains_tenant ON domains(tenant_id); +`, + }, + }, + { + name: "0002_users_auth", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE users ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + display_name TEXT, + role TEXT NOT NULL DEFAULT 'user', + active INTEGER NOT NULL DEFAULT 1, + mfa_enabled INTEGER NOT NULL DEFAULT 0, + totp_secret_enc BLOB, + passkey_credentials_json TEXT NOT NULL DEFAULT '[]', + quota_mb INTEGER NOT NULL DEFAULT 2048, + used_bytes INTEGER NOT NULL DEFAULT 0, + digest_enabled INTEGER NOT NULL DEFAULT 1, + digest_interval_mins INTEGER NOT NULL DEFAULT 0, + last_digest_at DATETIME, + last_login_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_users_tenant ON users(tenant_id); +CREATE INDEX idx_users_domain ON users(domain_id); + +CREATE TABLE app_passwords ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + label TEXT NOT NULL, + password_hash TEXT NOT NULL, + scopes TEXT NOT NULL DEFAULT 'smtp,imap', + last_used_at DATETIME, + expires_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_app_passwords_user ON app_passwords(user_id); + +CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + jti TEXT UNIQUE NOT NULL, + user_agent TEXT, + ip TEXT, + expires_at DATETIME NOT NULL, + revoked_at DATETIME +); +CREATE INDEX idx_sessions_user ON sessions(user_id); +CREATE INDEX idx_sessions_jti ON sessions(jti); + +CREATE TABLE aliases ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + from_address TEXT UNIQUE NOT NULL, + to_user_id TEXT REFERENCES users(id) ON DELETE CASCADE, + to_external TEXT, + active INTEGER NOT NULL DEFAULT 1 +); +CREATE INDEX idx_aliases_tenant ON aliases(tenant_id); +`, + }, + }, + { + name: "0003_list_rules", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE list_rules ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + list_type TEXT NOT NULL, + match_type TEXT NOT NULL DEFAULT 'email', + value TEXT NOT NULL, + note TEXT, + active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_list_rules_tenant ON list_rules(tenant_id); +CREATE INDEX idx_list_rules_value ON list_rules(value); +`, + }, + }, + { + name: "0004_messages_mailbox", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE messages ( + id TEXT PRIMARY KEY, + tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE, + from_address TEXT NOT NULL, + to_address TEXT NOT NULL, + subject TEXT, + message_id_hdr TEXT, + size_bytes INTEGER NOT NULL DEFAULT 0, + verdict TEXT NOT NULL DEFAULT 'clean', + total_score REAL NOT NULL DEFAULT 0, + sender_ip TEXT, + relayed_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_messages_tenant ON messages(tenant_id); +CREATE INDEX idx_messages_to ON messages(to_address); +CREATE INDEX idx_messages_created ON messages(created_at); + +CREATE TABLE message_checks ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + stage TEXT NOT NULL, + result TEXT NOT NULL, + score REAL NOT NULL DEFAULT 0, + detail TEXT, + duration_ms INTEGER NOT NULL DEFAULT 0 +); +CREATE INDEX idx_message_checks_message ON message_checks(message_id); + +CREATE TABLE mailbox_index ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + mailbox TEXT NOT NULL DEFAULT 'INBOX', + uid INTEGER NOT NULL, + eml_path TEXT NOT NULL, + flags TEXT NOT NULL DEFAULT '', + size_bytes INTEGER NOT NULL DEFAULT 0, + received_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + internal_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, mailbox, uid) +); +CREATE INDEX idx_mailbox_index_user ON mailbox_index(user_id, mailbox); + +CREATE TABLE mailbox_uid_counters ( + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + mailbox TEXT NOT NULL, + next_uid INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY (user_id, mailbox) +); +`, + }, + }, + { + name: "0005_outbound_queue", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE outbound_queue ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + from_address TEXT NOT NULL, + to_address TEXT NOT NULL, + eml_path TEXT NOT NULL, + priority INTEGER NOT NULL DEFAULT 0, + attempts INTEGER NOT NULL DEFAULT 0, + last_error TEXT, + next_attempt_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_outbound_queue_next ON outbound_queue(next_attempt_at); +CREATE INDEX idx_outbound_queue_user ON outbound_queue(user_id); +`, + }, + }, + { + name: "0006_quarantine", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE quarantine ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + eml_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'held', + reason TEXT, + released_by TEXT, + released_at DATETIME, + expires_at DATETIME NOT NULL, + notified_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_quarantine_status ON quarantine(status); +CREATE INDEX idx_quarantine_message ON quarantine(message_id); + +CREATE TABLE release_tokens ( + id TEXT PRIMARY KEY, + quarantine_id TEXT NOT NULL REFERENCES quarantine(id) ON DELETE CASCADE, + token TEXT UNIQUE NOT NULL, + email TEXT, + used_at DATETIME, + expires_at DATETIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_release_tokens_token ON release_tokens(token); +`, + }, + }, + { + name: "0007_linked_accounts", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE linked_accounts ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider TEXT NOT NULL, + display_name TEXT, + email_address TEXT NOT NULL, + auth_type TEXT NOT NULL, + imap_host TEXT, + imap_port INTEGER, + imap_tls TEXT, + smtp_host TEXT, + smtp_port INTEGER, + smtp_tls TEXT, + credential_enc BLOB, + oauth_expires_at DATETIME, + sync_state TEXT, + cache_retention_days INTEGER, + last_sync_at DATETIME, + last_sync_error TEXT, + active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_linked_accounts_user ON linked_accounts(user_id); +`, + }, + }, + { + name: "0008_dav_contacts_calendars", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE addressbooks ( + id TEXT PRIMARY KEY, + owner_type TEXT NOT NULL, + owner_id TEXT NOT NULL, + display_name TEXT, + description TEXT, + sync_token TEXT NOT NULL DEFAULT '1', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_addressbooks_owner ON addressbooks(owner_type, owner_id); + +CREATE TABLE contacts ( + id TEXT PRIMARY KEY, + addressbook_id TEXT NOT NULL REFERENCES addressbooks(id) ON DELETE CASCADE, + uid TEXT NOT NULL, + vcard_enc BLOB NOT NULL, + etag TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(addressbook_id, uid) +); +CREATE INDEX idx_contacts_addressbook ON contacts(addressbook_id); + +CREATE TABLE calendars ( + id TEXT PRIMARY KEY, + owner_type TEXT NOT NULL, + owner_id TEXT NOT NULL, + display_name TEXT, + description TEXT, + color TEXT, + timezone TEXT NOT NULL DEFAULT 'UTC', + sync_token TEXT NOT NULL DEFAULT '1', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_calendars_owner ON calendars(owner_type, owner_id); + +CREATE TABLE calendar_objects ( + id TEXT PRIMARY KEY, + calendar_id TEXT NOT NULL REFERENCES calendars(id) ON DELETE CASCADE, + uid TEXT NOT NULL, + ical_enc BLOB NOT NULL, + component_type TEXT, + summary TEXT, + dtstart DATETIME, + dtend DATETIME, + etag TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(calendar_id, uid) +); +CREATE INDEX idx_calendar_objects_calendar ON calendar_objects(calendar_id); +`, + }, + }, + { + name: "0009_sieve_scripts", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE sieve_scripts ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + script_text TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, name) +); +CREATE INDEX idx_sieve_scripts_user ON sieve_scripts(user_id); +`, + }, + }, + { + name: "0010_tls_certs", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE tls_certs ( + id TEXT PRIMARY KEY, + domain TEXT UNIQUE NOT NULL, + cert_pem_enc BLOB, + key_pem_enc BLOB, + expires_at DATETIME, + acme_account_key_enc BLOB, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_tls_certs_domain ON tls_certs(domain); +`, + }, + }, + { + name: "0011_mfa_and_recovery", + sql: map[string]string{ + "sqlite": ` +CREATE TABLE mfa_backup_codes ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + code_hash TEXT NOT NULL, + used_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_mfa_backup_codes_user ON mfa_backup_codes(user_id); + +ALTER TABLE users ADD COLUMN recovery_email TEXT; +`, + }, + }, +} diff --git a/internal/db/models.go b/internal/db/models.go new file mode 100644 index 0000000..deb5c0f --- /dev/null +++ b/internal/db/models.go @@ -0,0 +1,353 @@ +package db + +import "time" + +// ── Tenants & domains ────────────────────────────────────────────────────────── + +type Tenant struct { + ID string + Name string + DisplayName string + DigestIntervalMins int + MaxAccounts int // 0 = unlimited + QuotaMBPerUser int + SettingsJSON string // pipeline thresholds, check toggles (parsed by pipeline pkg) + CreatedAt time.Time +} + +type Domain struct { + ID string + TenantID string + Domain string + Active bool + DKIMSelector string + DKIMPrivateKeyEnc []byte // AES-256-GCM encrypted PEM + AcceptAll bool + CreatedAt time.Time +} + +// ── Users & auth ────────────────────────────────────────────────────────────── + +type UserRole string + +const ( + RoleUser UserRole = "user" + RoleTenantAdmin UserRole = "tenant_admin" + RoleGlobalAdmin UserRole = "global_admin" +) + +type User struct { + ID string + TenantID string + DomainID string + Email string + PasswordHash string + DisplayName string + Role UserRole + Active bool + MFAEnabled bool + TOTPSecretEnc []byte // AES-256-GCM encrypted + PasskeyCredentialsJSON string // JSON array of WebAuthn credentials + RecoveryEmail string // external address for password-reset delivery (see Phase 12 notes) + QuotaMB int + UsedBytes int64 + DigestEnabled bool + DigestIntervalMins int // 0 = use tenant default + LastDigestAt *time.Time + LastLoginAt *time.Time + CreatedAt time.Time +} + +type AppPassword struct { + ID string + UserID string + Label string + PasswordHash string // bcrypt of a 32-char random token + Scopes string // comma-separated: smtp,imap,caldav,carddav,pop3 + LastUsedAt *time.Time + ExpiresAt *time.Time // nil = never expires + CreatedAt time.Time +} + +type Session struct { + ID string + UserID string + JTI string // JWT ID, for revocation lookups + UserAgent string + IP string + ExpiresAt time.Time + RevokedAt *time.Time +} + +type Alias struct { + ID string + TenantID string + FromAddress string + ToUserID *string // nil if forwarding externally + ToExternal *string // nil if local + Active bool +} + +// ── List rules (allow/block, per tenant) ─────────────────────────────────────── + +type ListRuleAction string + +const ( + ListActionAllow ListRuleAction = "allow" + ListActionBlock ListRuleAction = "block" +) + +type ListRule struct { + ID string + TenantID string + ListType ListRuleAction // allow | block + MatchType string // email | domain + Value string + Note string + Active bool + CreatedAt time.Time +} + +// ── Messages (audit log) & mailbox index ──────────────────────────────────────── + +type MessageVerdict string + +const ( + VerdictClean MessageVerdict = "clean" + VerdictFlagged MessageVerdict = "flagged" + VerdictQuarantine MessageVerdict = "quarantine" + VerdictBlocked MessageVerdict = "blocked" +) + +type Message struct { + ID string + TenantID string + FromAddress string + ToAddress string + Subject string + MessageIDHdr string + SizeBytes int64 + Verdict MessageVerdict + TotalScore float64 + SenderIP string + RelayedAt *time.Time + CreatedAt time.Time +} + +type MailboxEntry struct { + ID string + UserID string + Mailbox string // INBOX, Sent, Trash, Junk, custom... + UID int + EMLPath string // path to encrypted .eml.enc on disk + Flags string // \Seen \Flagged \Answered \Deleted \Draft + SizeBytes int64 + ReceivedAt time.Time + InternalDate time.Time +} + +// ── Outbound queue ─────────────────────────────────────────────────────────── + +type OutboundQueueEntry struct { + ID string + UserID string + FromAddress string + ToAddress string + EMLPath string + Priority int + Attempts int + LastError string + NextAttemptAt time.Time + CreatedAt time.Time +} + +// ── Pipeline check results ────────────────────────────────────────────────────── + +// CheckResult is the outcome of a single pipeline stage (SPF, DKIM, etc.). +type CheckResult string + +const ( + CheckPass CheckResult = "pass" + CheckWarn CheckResult = "warn" + CheckFail CheckResult = "fail" + CheckSkipped CheckResult = "skipped" + CheckError CheckResult = "error" +) + +type MessageCheck struct { + ID string + MessageID string + Stage string + Result CheckResult + Score float64 + Detail string + DurationMs int64 +} + +// ── Quarantine ──────────────────────────────────────────────────────────────── + +type QuarantineStatus string + +const ( + QuarantineHeld QuarantineStatus = "held" + QuarantineReleased QuarantineStatus = "released" + QuarantineDeleted QuarantineStatus = "deleted" +) + +type QuarantineEntry struct { + ID string + MessageID string + EMLPath string + Status QuarantineStatus + Reason string + ReleasedBy string + ReleasedAt *time.Time + ExpiresAt time.Time + NotifiedAt *time.Time + CreatedAt time.Time +} + +type ReleaseToken struct { + ID string + QuarantineID string + Token string + Email string + UsedAt *time.Time + ExpiresAt time.Time + CreatedAt time.Time +} + +// ── Linked accounts (multi-account webmail — Part B of the plan) ────────────── + +type LinkedAccountProvider string + +const ( + ProviderGoMail LinkedAccountProvider = "gomail" + ProviderIMAP LinkedAccountProvider = "imap" + ProviderGmail LinkedAccountProvider = "gmail" // Phase 10 + ProviderM365 LinkedAccountProvider = "m365" // Phase 10 +) + +type LinkedAccountAuthType string + +const ( + AuthTypeSession LinkedAccountAuthType = "session" // gomail local account, already logged in + AuthTypePassword LinkedAccountAuthType = "password" // generic IMAP/SMTP + AuthTypeOAuth2 LinkedAccountAuthType = "oauth2" // Phase 10 +) + +type LinkedAccount struct { + ID string + UserID string + Provider LinkedAccountProvider + DisplayName string + EmailAddress string + AuthType LinkedAccountAuthType + IMAPHost string + IMAPPort int + IMAPTLS string // "starttls" | "implicit" | "off" + SMTPHost string + SMTPPort int + SMTPTLS string + CredentialEnc []byte // encrypted password or OAuth2 tokens (JSON) + OAuthExpiresAt *time.Time + SyncState string + CacheRetentionDays int // 0 = use instance default + LastSyncAt *time.Time + LastSyncError string + Active bool + CreatedAt time.Time +} + +// ── CalDAV / CardDAV ──────────────────────────────────────────────────────────── + +// OwnerType distinguishes a personal (per-user) collection from a shared +// tenant-wide one — both addressbooks and calendars support both scopes per +// the plan (tenant addressbook + per-user addressbook, same for calendars). +type OwnerType string + +const ( + OwnerUser OwnerType = "user" + OwnerTenant OwnerType = "tenant" +) + +type Addressbook struct { + ID string + OwnerType OwnerType + OwnerID string + DisplayName string + Description string + SyncToken string + CreatedAt time.Time +} + +type Contact struct { + ID string + AddressbookID string + UID string + VCardEnc []byte // AES-256-GCM encrypted vCard text + ETag string + CreatedAt time.Time + UpdatedAt time.Time +} + +type Calendar struct { + ID string + OwnerType OwnerType + OwnerID string + DisplayName string + Description string + Color string + Timezone string + SyncToken string + CreatedAt time.Time +} + +type CalendarObject struct { + ID string + CalendarID string + UID string + ICalEnc []byte // AES-256-GCM encrypted iCal text + ComponentType string // VEVENT | VTODO | VJOURNAL + Summary string + DTStart *time.Time + DTEnd *time.Time + ETag string + CreatedAt time.Time + UpdatedAt time.Time +} + +// ── ManageSieve ─────────────────────────────────────────────────────────────── + +type SieveScript struct { + ID string + UserID string + Name string + ScriptText string + Active bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// ── TLS certs (ACME) ──────────────────────────────────────────────────────────── + +type TLSCert struct { + ID string + Domain string + CertPEMEnc []byte + KeyPEMEnc []byte + ExpiresAt *time.Time + ACMEAccountKeyEnc []byte + CreatedAt time.Time + UpdatedAt time.Time +} + +// ── MFA ─────────────────────────────────────────────────────────────────────── + +type MFABackupCode struct { + ID string + UserID string + CodeHash string + UsedAt *time.Time + CreatedAt time.Time +} diff --git a/internal/db/queries.go b/internal/db/queries.go new file mode 100644 index 0000000..a06d5a9 --- /dev/null +++ b/internal/db/queries.go @@ -0,0 +1,1182 @@ +package db + +import ( + "crypto/subtle" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" +) + +func uuidNew() string { return uuid.NewString() } + +// LookupDomain finds an active domain by name, along with its tenant. +// Returns ErrNotFound if the domain is not hosted here or is inactive. +func (db *DB) LookupDomain(domain string) (*Domain, *Tenant, error) { + row := db.QueryRow(` + SELECT d.id, d.tenant_id, d.domain, d.active, d.dkim_selector, d.dkim_private_key_enc, d.accept_all, d.created_at, + t.id, t.name, t.display_name, t.digest_interval_mins, t.max_accounts, t.quota_mb_per_user, t.settings_json, t.created_at + FROM domains d + JOIN tenants t ON t.id = d.tenant_id + WHERE d.domain = ? AND d.active = 1 + `, domain) + + var d Domain + var t Tenant + var dkimSelector, tenantDisplayName sql.NullString + err := row.Scan( + &d.ID, &d.TenantID, &d.Domain, &d.Active, &dkimSelector, &d.DKIMPrivateKeyEnc, &d.AcceptAll, &d.CreatedAt, + &t.ID, &t.Name, &tenantDisplayName, &t.DigestIntervalMins, &t.MaxAccounts, &t.QuotaMBPerUser, &t.SettingsJSON, &t.CreatedAt, + ) + d.DKIMSelector = dkimSelector.String + t.DisplayName = tenantDisplayName.String + if err == sql.ErrNoRows { + return nil, nil, ErrNotFound + } + if err != nil { + return nil, nil, fmt.Errorf("lookup domain %q: %w", domain, err) + } + return &d, &t, nil +} + +// LookupUserByEmail finds an active user by email address. +func (db *DB) LookupUserByEmail(email string) (*User, error) { + row := db.QueryRow(` + SELECT id, tenant_id, domain_id, email, password_hash, display_name, role, active, + mfa_enabled, totp_secret_enc, recovery_email, quota_mb, used_bytes, digest_enabled, digest_interval_mins, created_at + FROM users WHERE email = ? AND active = 1 + `, email) + + var u User + var displayName, recoveryEmail sql.NullString + err := row.Scan( + &u.ID, &u.TenantID, &u.DomainID, &u.Email, &u.PasswordHash, &displayName, &u.Role, &u.Active, + &u.MFAEnabled, &u.TOTPSecretEnc, &recoveryEmail, &u.QuotaMB, &u.UsedBytes, &u.DigestEnabled, &u.DigestIntervalMins, &u.CreatedAt, + ) + u.DisplayName = displayName.String + u.RecoveryEmail = recoveryEmail.String + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("lookup user %q: %w", email, err) + } + return &u, nil +} + +// LookupAlias resolves an alias address to its target (local user or external). +func (db *DB) LookupAlias(fromAddress string) (*Alias, error) { + row := db.QueryRow(` + SELECT id, tenant_id, from_address, to_user_id, to_external, active + FROM aliases WHERE from_address = ? AND active = 1 + `, fromAddress) + + var a Alias + err := row.Scan(&a.ID, &a.TenantID, &a.FromAddress, &a.ToUserID, &a.ToExternal, &a.Active) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("lookup alias %q: %w", fromAddress, err) + } + return &a, nil +} + +// MatchListRule checks a sender address+domain against the tenant's allow/block +// rules. Returns (matched, action) — allow rules are checked first so an +// explicit allow always wins over a domain-level block. +func (db *DB) MatchListRule(tenantID, fromAddress, fromDomain string) (bool, ListRuleAction, error) { + rows, err := db.Query(` + SELECT list_type, match_type, value FROM list_rules + WHERE tenant_id = ? AND active = 1 + `, tenantID) + if err != nil { + return false, "", fmt.Errorf("query list rules: %w", err) + } + defer rows.Close() + + sawBlock := false + for rows.Next() { + var listType ListRuleAction + var matchType, value string + if err := rows.Scan(&listType, &matchType, &value); err != nil { + continue + } + matched := (matchType == "email" && value == fromAddress) || + (matchType == "domain" && value == fromDomain) + if !matched { + continue + } + if listType == ListActionAllow { + return true, ListActionAllow, nil // allow wins immediately + } + sawBlock = true + } + if sawBlock { + return true, ListActionBlock, nil + } + return false, "", nil +} + +// InsertMessage records an audit-log row for a processed message and returns its ID. +func (db *DB) InsertMessage(m *Message) error { + _, err := db.Exec(` + INSERT INTO messages (id, tenant_id, from_address, to_address, subject, message_id_hdr, + size_bytes, verdict, total_score, sender_ip, relayed_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, m.ID, m.TenantID, m.FromAddress, m.ToAddress, m.Subject, m.MessageIDHdr, + m.SizeBytes, m.Verdict, m.TotalScore, m.SenderIP, m.RelayedAt) + if err != nil { + return fmt.Errorf("insert message: %w", err) + } + return nil +} + +// UpdateMessageVerdict updates a message row's verdict, score, and delivery +// timestamp after the pipeline has run — called after the initial insert so +// message_checks rows (which FK-reference messages.id) always have a valid +// parent row to attach to, regardless of how long pipeline evaluation takes. +func (db *DB) UpdateMessageVerdict(id string, verdict MessageVerdict, score float64, relayedAt *time.Time) error { + _, err := db.Exec(`UPDATE messages SET verdict = ?, total_score = ?, relayed_at = ? WHERE id = ?`, + verdict, score, relayedAt, id) + return err +} + +// InsertMessageCheck records one pipeline stage's result for a message. +func (db *DB) InsertMessageCheck(c *MessageCheck) error { + _, err := db.Exec(` + INSERT INTO message_checks (id, message_id, stage, result, score, detail, duration_ms) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, c.ID, c.MessageID, c.Stage, c.Result, c.Score, c.Detail, c.DurationMs) + if err != nil { + return fmt.Errorf("insert message check: %w", err) + } + return nil +} + +// ── Quarantine ──────────────────────────────────────────────────────────────── + +// InsertQuarantineEntry holds a message for review instead of delivering it. +func (db *DB) InsertQuarantineEntry(e *QuarantineEntry) error { + _, err := db.Exec(` + INSERT INTO quarantine (id, message_id, eml_path, status, reason, expires_at) + VALUES (?, ?, ?, ?, ?, ?) + `, e.ID, e.MessageID, e.EMLPath, e.Status, e.Reason, e.ExpiresAt) + if err != nil { + return fmt.Errorf("insert quarantine entry: %w", err) + } + return nil +} + +// QuarantineEntriesForUser returns held quarantine entries whose underlying +// message was addressed to the given recipient — used both by the webmail +// quarantine view (later phase) and the digest notifier (this phase). +func (db *DB) QuarantineEntriesForUser(toAddress string, since time.Time) ([]QuarantineEntry, error) { + rows, err := db.Query(` + SELECT q.id, q.message_id, q.eml_path, q.status, q.reason, q.expires_at, q.created_at + FROM quarantine q + JOIN messages m ON m.id = q.message_id + WHERE m.to_address = ? AND q.status = ? AND q.created_at > ? + ORDER BY q.created_at DESC + `, toAddress, QuarantineHeld, since) + if err != nil { + return nil, fmt.Errorf("query quarantine for user: %w", err) + } + defer rows.Close() + + var entries []QuarantineEntry + for rows.Next() { + var e QuarantineEntry + var reason sql.NullString + if err := rows.Scan(&e.ID, &e.MessageID, &e.EMLPath, &e.Status, &reason, &e.ExpiresAt, &e.CreatedAt); err != nil { + continue + } + e.Reason = reason.String + entries = append(entries, e) + } + return entries, nil +} + +// ReleaseQuarantineEntry marks an entry released — the caller (webmail API in +// a later phase, or the digest's release-link handler) is responsible for +// actually delivering the underlying message to the recipient's mailbox. +func (db *DB) ReleaseQuarantineEntry(id, releasedBy string) error { + _, err := db.Exec(` + UPDATE quarantine SET status = ?, released_by = ?, released_at = ? WHERE id = ? AND status = ? + `, QuarantineReleased, releasedBy, time.Now().UTC(), id, QuarantineHeld) + return err +} + +func (db *DB) GetQuarantineEntry(id string) (*QuarantineEntry, error) { + row := db.QueryRow(` + SELECT id, message_id, eml_path, status, reason, expires_at, created_at + FROM quarantine WHERE id = ? + `, id) + var e QuarantineEntry + var reason sql.NullString + err := row.Scan(&e.ID, &e.MessageID, &e.EMLPath, &e.Status, &reason, &e.ExpiresAt, &e.CreatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get quarantine entry: %w", err) + } + e.Reason = reason.String + return &e, nil +} + +// InsertReleaseToken creates a time-limited token for one-click release from +// a digest email link. +func (db *DB) InsertReleaseToken(t *ReleaseToken) error { + _, err := db.Exec(` + INSERT INTO release_tokens (id, quarantine_id, token, email, expires_at) + VALUES (?, ?, ?, ?, ?) + `, t.ID, t.QuarantineID, t.Token, t.Email, t.ExpiresAt) + return err +} + +func (db *DB) LookupReleaseToken(token string) (*ReleaseToken, error) { + row := db.QueryRow(` + SELECT id, quarantine_id, token, email, used_at, expires_at, created_at + FROM release_tokens WHERE token = ? AND used_at IS NULL AND expires_at > ? + `, token, time.Now().UTC()) + var t ReleaseToken + err := row.Scan(&t.ID, &t.QuarantineID, &t.Token, &t.Email, &t.UsedAt, &t.ExpiresAt, &t.CreatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("lookup release token: %w", err) + } + return &t, nil +} + +func (db *DB) MarkReleaseTokenUsed(id string) error { + _, err := db.Exec(`UPDATE release_tokens SET used_at = ? WHERE id = ?`, time.Now().UTC(), id) + return err +} + +// NextMailboxUID atomically allocates the next IMAP UID for a user's mailbox. +func (db *DB) NextMailboxUID(userID, mailbox string) (int, error) { + tx, err := db.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + _, err = tx.Exec(` + INSERT INTO mailbox_uid_counters (user_id, mailbox, next_uid) VALUES (?, ?, 2) + ON CONFLICT(user_id, mailbox) DO UPDATE SET next_uid = next_uid + 1 + `, userID, mailbox) + if err != nil { + return 0, fmt.Errorf("allocate uid: %w", err) + } + + var next int + if err := tx.QueryRow(`SELECT next_uid FROM mailbox_uid_counters WHERE user_id = ? AND mailbox = ?`, + userID, mailbox).Scan(&next); err != nil { + return 0, fmt.Errorf("read allocated uid: %w", err) + } + + if err := tx.Commit(); err != nil { + return 0, err + } + return next - 1, nil // the UID just consumed +} + +// InsertMailboxEntry records a delivered message in a user's mailbox index. +func (db *DB) InsertMailboxEntry(e *MailboxEntry) error { + _, err := db.Exec(` + INSERT INTO mailbox_index (id, user_id, mailbox, uid, eml_path, flags, size_bytes, received_at, internal_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, e.ID, e.UserID, e.Mailbox, e.UID, e.EMLPath, e.Flags, e.SizeBytes, e.ReceivedAt, e.InternalDate) + if err != nil { + return fmt.Errorf("insert mailbox entry: %w", err) + } + return nil +} + +// ListMailboxEntries returns all messages in a user's mailbox, ordered by +// UID ascending — the order IMAP sequence numbers are defined against. +func (db *DB) ListMailboxEntries(userID, mailbox string) ([]MailboxEntry, error) { + rows, err := db.Query(` + SELECT id, user_id, mailbox, uid, eml_path, flags, size_bytes, received_at, internal_date + FROM mailbox_index WHERE user_id = ? AND mailbox = ? ORDER BY uid ASC + `, userID, mailbox) + if err != nil { + return nil, fmt.Errorf("list mailbox entries: %w", err) + } + defer rows.Close() + + var entries []MailboxEntry + for rows.Next() { + var e MailboxEntry + if err := rows.Scan(&e.ID, &e.UserID, &e.Mailbox, &e.UID, &e.EMLPath, &e.Flags, &e.SizeBytes, &e.ReceivedAt, &e.InternalDate); err != nil { + continue + } + entries = append(entries, e) + } + return entries, nil +} + +// ListMailboxNames returns the distinct mailbox (folder) names a user has — +// always includes INBOX even if currently empty, since every account has one. +func (db *DB) ListMailboxNames(userID string) ([]string, error) { + rows, err := db.Query(`SELECT DISTINCT mailbox FROM mailbox_index WHERE user_id = ? ORDER BY mailbox`, userID) + if err != nil { + return nil, fmt.Errorf("list mailbox names: %w", err) + } + defer rows.Close() + + seen := map[string]bool{"INBOX": true} + names := []string{"INBOX"} + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + continue + } + if !seen[name] { + seen[name] = true + names = append(names, name) + } + } + return names, nil +} + +// UpdateMailboxFlags overwrites the flag string for one message. +func (db *DB) UpdateMailboxFlags(id, flags string) error { + _, err := db.Exec(`UPDATE mailbox_index SET flags = ? WHERE id = ?`, flags, id) + return err +} + +// DeleteMailboxEntry removes one message from the index (used by EXPUNGE and +// POP3 DELE) — the caller is responsible for also removing the on-disk file. +func (db *DB) DeleteMailboxEntry(id string) error { + _, err := db.Exec(`DELETE FROM mailbox_index WHERE id = ?`, id) + return err +} + +// ── Linked accounts ────────────────────────────────────────────────────────── + +func (db *DB) InsertLinkedAccount(a *LinkedAccount) error { + _, err := db.Exec(` + INSERT INTO linked_accounts (id, user_id, provider, display_name, email_address, auth_type, + imap_host, imap_port, imap_tls, smtp_host, smtp_port, smtp_tls, credential_enc, active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) + `, a.ID, a.UserID, a.Provider, a.DisplayName, a.EmailAddress, a.AuthType, + a.IMAPHost, a.IMAPPort, a.IMAPTLS, a.SMTPHost, a.SMTPPort, a.SMTPTLS, a.CredentialEnc) + if err != nil { + return fmt.Errorf("insert linked account: %w", err) + } + return nil +} + +func (db *DB) ListLinkedAccounts(userID string) ([]LinkedAccount, error) { + rows, err := db.Query(` + SELECT id, user_id, provider, display_name, email_address, auth_type, + imap_host, imap_port, imap_tls, smtp_host, smtp_port, smtp_tls, + credential_enc, sync_state, cache_retention_days, active, created_at + FROM linked_accounts WHERE user_id = ? AND active = 1 + `, userID) + if err != nil { + return nil, fmt.Errorf("list linked accounts: %w", err) + } + defer rows.Close() + + var out []LinkedAccount + for rows.Next() { + var a LinkedAccount + var displayName, imapHost, smtpHost, imapTLS, smtpTLS, syncState sql.NullString + var imapPort, smtpPort, cacheDays sql.NullInt64 + if err := rows.Scan(&a.ID, &a.UserID, &a.Provider, &displayName, &a.EmailAddress, &a.AuthType, + &imapHost, &imapPort, &imapTLS, &smtpHost, &smtpPort, &smtpTLS, + &a.CredentialEnc, &syncState, &cacheDays, &a.Active, &a.CreatedAt); err != nil { + continue + } + a.DisplayName = displayName.String + a.IMAPHost = imapHost.String + a.IMAPPort = int(imapPort.Int64) + a.IMAPTLS = imapTLS.String + a.SMTPHost = smtpHost.String + a.SMTPPort = int(smtpPort.Int64) + a.SMTPTLS = smtpTLS.String + a.SyncState = syncState.String + a.CacheRetentionDays = int(cacheDays.Int64) + out = append(out, a) + } + return out, nil +} + +func (db *DB) GetLinkedAccount(id string) (*LinkedAccount, error) { + row := db.QueryRow(` + SELECT id, user_id, provider, display_name, email_address, auth_type, + imap_host, imap_port, imap_tls, smtp_host, smtp_port, smtp_tls, + credential_enc, sync_state, cache_retention_days, active, created_at + FROM linked_accounts WHERE id = ? + `, id) + var a LinkedAccount + var displayName, imapHost, smtpHost, imapTLS, smtpTLS, syncState sql.NullString + var imapPort, smtpPort, cacheDays sql.NullInt64 + err := row.Scan(&a.ID, &a.UserID, &a.Provider, &displayName, &a.EmailAddress, &a.AuthType, + &imapHost, &imapPort, &imapTLS, &smtpHost, &smtpPort, &smtpTLS, + &a.CredentialEnc, &syncState, &cacheDays, &a.Active, &a.CreatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get linked account: %w", err) + } + a.DisplayName = displayName.String + a.IMAPHost = imapHost.String + a.IMAPPort = int(imapPort.Int64) + a.IMAPTLS = imapTLS.String + a.SMTPHost = smtpHost.String + a.SMTPPort = int(smtpPort.Int64) + a.SMTPTLS = smtpTLS.String + a.SyncState = syncState.String + a.CacheRetentionDays = int(cacheDays.Int64) + return &a, nil +} + +func (db *DB) UpdateLinkedAccountSync(id, syncState string, syncErr string) error { + _, err := db.Exec(`UPDATE linked_accounts SET sync_state = ?, last_sync_at = ?, last_sync_error = ? WHERE id = ?`, + syncState, time.Now().UTC(), syncErr, id) + return err +} + +func (db *DB) DeactivateLinkedAccount(id string) error { + _, err := db.Exec(`UPDATE linked_accounts SET active = 0 WHERE id = ?`, id) + return err +} + +var ErrNotFound = fmt.Errorf("not found") + +// ── Admin: tenants ────────────────────────────────────────────────────────────── + +func (db *DB) ListTenants() ([]Tenant, error) { + rows, err := db.Query(`SELECT id, name, display_name, digest_interval_mins, max_accounts, quota_mb_per_user, settings_json, created_at FROM tenants ORDER BY name`) + if err != nil { + return nil, fmt.Errorf("list tenants: %w", err) + } + defer rows.Close() + var out []Tenant + for rows.Next() { + var t Tenant + var displayName sql.NullString + if err := rows.Scan(&t.ID, &t.Name, &displayName, &t.DigestIntervalMins, &t.MaxAccounts, &t.QuotaMBPerUser, &t.SettingsJSON, &t.CreatedAt); err != nil { + continue + } + t.DisplayName = displayName.String + out = append(out, t) + } + return out, nil +} + +func (db *DB) CreateTenant(t *Tenant) error { + _, err := db.Exec(`INSERT INTO tenants (id, name, display_name) VALUES (?, ?, ?)`, t.ID, t.Name, t.DisplayName) + return err +} + +// ── Admin: domains ──────────────────────────────────────────────────────────── + +func (db *DB) ListDomains() ([]Domain, error) { + rows, err := db.Query(`SELECT id, tenant_id, domain, active, dkim_selector, dkim_private_key_enc, accept_all, created_at FROM domains ORDER BY domain`) + if err != nil { + return nil, fmt.Errorf("list domains: %w", err) + } + defer rows.Close() + var out []Domain + for rows.Next() { + var d Domain + var dkimSelector sql.NullString + if err := rows.Scan(&d.ID, &d.TenantID, &d.Domain, &d.Active, &dkimSelector, &d.DKIMPrivateKeyEnc, &d.AcceptAll, &d.CreatedAt); err != nil { + continue + } + d.DKIMSelector = dkimSelector.String + out = append(out, d) + } + return out, nil +} + +func (db *DB) CreateDomain(d *Domain) error { + _, err := db.Exec(`INSERT INTO domains (id, tenant_id, domain, active, accept_all, dkim_selector, dkim_private_key_enc) VALUES (?, ?, ?, 1, 1, ?, ?)`, + d.ID, d.TenantID, d.Domain, d.DKIMSelector, d.DKIMPrivateKeyEnc) + return err +} + +func (db *DB) UpdateDomainDKIMKey(id, selector string, keyEnc []byte) error { + _, err := db.Exec(`UPDATE domains SET dkim_selector = ?, dkim_private_key_enc = ? WHERE id = ?`, selector, keyEnc, id) + return err +} + +func (db *DB) DeleteDomain(id string) error { + _, err := db.Exec(`DELETE FROM domains WHERE id = ?`, id) + return err +} + +func (db *DB) GetDomain(id string) (*Domain, error) { + row := db.QueryRow(`SELECT id, tenant_id, domain, active, dkim_selector, dkim_private_key_enc, accept_all, created_at FROM domains WHERE id = ?`, id) + var d Domain + var dkimSelector sql.NullString + err := row.Scan(&d.ID, &d.TenantID, &d.Domain, &d.Active, &dkimSelector, &d.DKIMPrivateKeyEnc, &d.AcceptAll, &d.CreatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get domain: %w", err) + } + d.DKIMSelector = dkimSelector.String + return &d, nil +} + +// ── Admin: users ────────────────────────────────────────────────────────────── + +// ListUsers returns every user, optionally filtered to one tenant +// (tenantID == "" means all tenants — global_admin view). +func (db *DB) ListUsers(tenantID string) ([]User, error) { + query := `SELECT id, tenant_id, domain_id, email, display_name, role, active, mfa_enabled, quota_mb, used_bytes, created_at FROM users` + args := []any{} + if tenantID != "" { + query += ` WHERE tenant_id = ?` + args = append(args, tenantID) + } + query += ` ORDER BY email` + + rows, err := db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("list users: %w", err) + } + defer rows.Close() + var out []User + for rows.Next() { + var u User + var displayName sql.NullString + if err := rows.Scan(&u.ID, &u.TenantID, &u.DomainID, &u.Email, &displayName, &u.Role, &u.Active, &u.MFAEnabled, &u.QuotaMB, &u.UsedBytes, &u.CreatedAt); err != nil { + continue + } + u.DisplayName = displayName.String + out = append(out, u) + } + return out, nil +} + +func (db *DB) CreateUser(u *User, passwordHash string) error { + _, err := db.Exec(`INSERT INTO users (id, tenant_id, domain_id, email, password_hash, display_name, role, active) VALUES (?, ?, ?, ?, ?, ?, ?, 1)`, + u.ID, u.TenantID, u.DomainID, u.Email, passwordHash, u.DisplayName, u.Role) + return err +} + +func (db *DB) SetUserActive(id string, active bool) error { + _, err := db.Exec(`UPDATE users SET active = ? WHERE id = ?`, active, id) + return err +} + +func (db *DB) SetUserPassword(id, passwordHash string) error { + _, err := db.Exec(`UPDATE users SET password_hash = ? WHERE id = ?`, passwordHash, id) + return err +} + +func (db *DB) DeleteUser(id string) error { + _, err := db.Exec(`DELETE FROM users WHERE id = ?`, id) + return err +} + +func (db *DB) GetUser(id string) (*User, error) { + row := db.QueryRow(`SELECT id, tenant_id, domain_id, email, password_hash, display_name, role, active, + mfa_enabled, totp_secret_enc, recovery_email FROM users WHERE id = ?`, id) + var u User + var displayName, recoveryEmail sql.NullString + err := row.Scan(&u.ID, &u.TenantID, &u.DomainID, &u.Email, &u.PasswordHash, &displayName, &u.Role, &u.Active, + &u.MFAEnabled, &u.TOTPSecretEnc, &recoveryEmail) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get user: %w", err) + } + u.DisplayName = displayName.String + u.RecoveryEmail = recoveryEmail.String + return &u, nil +} + +// ── Admin: list rules ───────────────────────────────────────────────────────── + +func (db *DB) ListListRules(tenantID string) ([]ListRule, error) { + rows, err := db.Query(`SELECT id, tenant_id, list_type, match_type, value, note, active, created_at FROM list_rules WHERE tenant_id = ? ORDER BY created_at DESC`, tenantID) + if err != nil { + return nil, fmt.Errorf("list list_rules: %w", err) + } + defer rows.Close() + var out []ListRule + for rows.Next() { + var r ListRule + var note sql.NullString + if err := rows.Scan(&r.ID, &r.TenantID, &r.ListType, &r.MatchType, &r.Value, ¬e, &r.Active, &r.CreatedAt); err != nil { + continue + } + r.Note = note.String + out = append(out, r) + } + return out, nil +} + +func (db *DB) CreateListRule(r *ListRule) error { + _, err := db.Exec(`INSERT INTO list_rules (id, tenant_id, list_type, match_type, value, note, active) VALUES (?, ?, ?, ?, ?, ?, 1)`, + r.ID, r.TenantID, r.ListType, r.MatchType, r.Value, r.Note) + return err +} + +func (db *DB) DeleteListRule(id string) error { + _, err := db.Exec(`DELETE FROM list_rules WHERE id = ?`, id) + return err +} + +// ── Admin: outbound queue ──────────────────────────────────────────────────── + +func (db *DB) ListAllOutboundQueue() ([]OutboundQueueEntry, error) { + rows, err := db.Query(`SELECT id, user_id, from_address, to_address, eml_path, priority, attempts, last_error, next_attempt_at, created_at FROM outbound_queue ORDER BY created_at DESC`) + if err != nil { + return nil, fmt.Errorf("list outbound queue: %w", err) + } + defer rows.Close() + var out []OutboundQueueEntry + for rows.Next() { + var e OutboundQueueEntry + var lastError sql.NullString + if err := rows.Scan(&e.ID, &e.UserID, &e.FromAddress, &e.ToAddress, &e.EMLPath, &e.Priority, &e.Attempts, &lastError, &e.NextAttemptAt, &e.CreatedAt); err != nil { + continue + } + e.LastError = lastError.String + out = append(out, e) + } + return out, nil +} + +// RetryQueueEntryNow resets a queue entry's schedule to immediately, for the +// admin "retry now" button — the queue worker's next poll picks it up. +func (db *DB) RetryQueueEntryNow(id string) error { + _, err := db.Exec(`UPDATE outbound_queue SET next_attempt_at = ? WHERE id = ?`, time.Now().UTC(), id) + return err +} + +// ── Admin: quarantine (global) ─────────────────────────────────────────────── + +// DeleteQuarantineEntry marks a held message as permanently discarded — used +// by the admin portal's global quarantine view (per-user release, as +// opposed to discard, is handled by webmail's own quarantine endpoint). +func (db *DB) DeleteQuarantineEntry(id string) error { + _, err := db.Exec(`UPDATE quarantine SET status = 'deleted' WHERE id = ?`, id) + return err +} + +func (db *DB) ListAllQuarantine() ([]QuarantineEntry, error) { + rows, err := db.Query(`SELECT id, message_id, eml_path, status, reason, expires_at, created_at FROM quarantine WHERE status = 'held' ORDER BY created_at DESC`) + if err != nil { + return nil, fmt.Errorf("list quarantine: %w", err) + } + defer rows.Close() + var out []QuarantineEntry + for rows.Next() { + var e QuarantineEntry + var reason sql.NullString + if err := rows.Scan(&e.ID, &e.MessageID, &e.EMLPath, &e.Status, &reason, &e.ExpiresAt, &e.CreatedAt); err != nil { + continue + } + e.Reason = reason.String + out = append(out, e) + } + return out, nil +} + +// ── Admin: dashboard stats ──────────────────────────────────────────────────── + +type Stats struct { + TotalUsers int + TotalDomains int + Messages24h int + QueueDepth int + QuarantineHeld int +} + +func (db *DB) GetStats() (*Stats, error) { + s := &Stats{} + db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&s.TotalUsers) + db.QueryRow(`SELECT COUNT(*) FROM domains`).Scan(&s.TotalDomains) + db.QueryRow(`SELECT COUNT(*) FROM messages WHERE created_at > ?`, time.Now().UTC().Add(-24*time.Hour)).Scan(&s.Messages24h) + db.QueryRow(`SELECT COUNT(*) FROM outbound_queue`).Scan(&s.QueueDepth) + db.QueryRow(`SELECT COUNT(*) FROM quarantine WHERE status = 'held'`).Scan(&s.QuarantineHeld) + return s, nil +} + +// ── CalDAV / CardDAV ──────────────────────────────────────────────────────────── + +func (db *DB) GetOrCreateAddressbook(ownerType OwnerType, ownerID, displayName string) (*Addressbook, error) { + row := db.QueryRow(`SELECT id, owner_type, owner_id, display_name, description, sync_token, created_at + FROM addressbooks WHERE owner_type = ? AND owner_id = ?`, ownerType, ownerID) + var a Addressbook + var desc sql.NullString + err := row.Scan(&a.ID, &a.OwnerType, &a.OwnerID, &a.DisplayName, &desc, &a.SyncToken, &a.CreatedAt) + if err == nil { + a.Description = desc.String + return &a, nil + } + if err != sql.ErrNoRows { + return nil, fmt.Errorf("get addressbook: %w", err) + } + + a = Addressbook{ID: uuidNew(), OwnerType: ownerType, OwnerID: ownerID, DisplayName: displayName, SyncToken: "1"} + if _, err := db.Exec(`INSERT INTO addressbooks (id, owner_type, owner_id, display_name, sync_token) VALUES (?, ?, ?, ?, ?)`, + a.ID, a.OwnerType, a.OwnerID, a.DisplayName, a.SyncToken); err != nil { + return nil, fmt.Errorf("create addressbook: %w", err) + } + return &a, nil +} + +func (db *DB) GetOrCreateCalendar(ownerType OwnerType, ownerID, displayName string) (*Calendar, error) { + row := db.QueryRow(`SELECT id, owner_type, owner_id, display_name, description, color, timezone, sync_token, created_at + FROM calendars WHERE owner_type = ? AND owner_id = ?`, ownerType, ownerID) + var c Calendar + var desc, color sql.NullString + err := row.Scan(&c.ID, &c.OwnerType, &c.OwnerID, &c.DisplayName, &desc, &color, &c.Timezone, &c.SyncToken, &c.CreatedAt) + if err == nil { + c.Description = desc.String + c.Color = color.String + return &c, nil + } + if err != sql.ErrNoRows { + return nil, fmt.Errorf("get calendar: %w", err) + } + + c = Calendar{ID: uuidNew(), OwnerType: ownerType, OwnerID: ownerID, DisplayName: displayName, Timezone: "UTC", SyncToken: "1"} + if _, err := db.Exec(`INSERT INTO calendars (id, owner_type, owner_id, display_name, timezone, sync_token) VALUES (?, ?, ?, ?, ?, ?)`, + c.ID, c.OwnerType, c.OwnerID, c.DisplayName, c.Timezone, c.SyncToken); err != nil { + return nil, fmt.Errorf("create calendar: %w", err) + } + return &c, nil +} + +func (db *DB) ListContacts(addressbookID string) ([]Contact, error) { + rows, err := db.Query(`SELECT id, addressbook_id, uid, vcard_enc, etag, created_at, updated_at + FROM contacts WHERE addressbook_id = ? ORDER BY uid`, addressbookID) + if err != nil { + return nil, fmt.Errorf("list contacts: %w", err) + } + defer rows.Close() + var out []Contact + for rows.Next() { + var c Contact + if err := rows.Scan(&c.ID, &c.AddressbookID, &c.UID, &c.VCardEnc, &c.ETag, &c.CreatedAt, &c.UpdatedAt); err != nil { + continue + } + out = append(out, c) + } + return out, nil +} + +func (db *DB) GetContact(addressbookID, uid string) (*Contact, error) { + row := db.QueryRow(`SELECT id, addressbook_id, uid, vcard_enc, etag, created_at, updated_at + FROM contacts WHERE addressbook_id = ? AND uid = ?`, addressbookID, uid) + var c Contact + err := row.Scan(&c.ID, &c.AddressbookID, &c.UID, &c.VCardEnc, &c.ETag, &c.CreatedAt, &c.UpdatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get contact: %w", err) + } + return &c, nil +} + +// UpsertContact creates or updates a contact by (addressbookID, uid), bumping +// the etag — used by PUT, which per WebDAV semantics both creates new +// resources and updates existing ones at the same URL. +func (db *DB) UpsertContact(c *Contact) error { + existing, err := db.GetContact(c.AddressbookID, c.UID) + if err == nil { + _, err := db.Exec(`UPDATE contacts SET vcard_enc = ?, etag = ?, updated_at = ? WHERE id = ?`, + c.VCardEnc, c.ETag, time.Now().UTC(), existing.ID) + return err + } + _, err = db.Exec(`INSERT INTO contacts (id, addressbook_id, uid, vcard_enc, etag) VALUES (?, ?, ?, ?, ?)`, + c.ID, c.AddressbookID, c.UID, c.VCardEnc, c.ETag) + return err +} + +func (db *DB) DeleteContact(addressbookID, uid string) error { + _, err := db.Exec(`DELETE FROM contacts WHERE addressbook_id = ? AND uid = ?`, addressbookID, uid) + return err +} + +func (db *DB) ListCalendarObjects(calendarID string) ([]CalendarObject, error) { + rows, err := db.Query(`SELECT id, calendar_id, uid, ical_enc, component_type, summary, dtstart, dtend, etag, created_at, updated_at + FROM calendar_objects WHERE calendar_id = ? ORDER BY dtstart`, calendarID) + if err != nil { + return nil, fmt.Errorf("list calendar objects: %w", err) + } + defer rows.Close() + var out []CalendarObject + for rows.Next() { + var c CalendarObject + var componentType, summary sql.NullString + var dtstart, dtend sql.NullTime + if err := rows.Scan(&c.ID, &c.CalendarID, &c.UID, &c.ICalEnc, &componentType, &summary, &dtstart, &dtend, &c.ETag, &c.CreatedAt, &c.UpdatedAt); err != nil { + continue + } + c.ComponentType = componentType.String + c.Summary = summary.String + if dtstart.Valid { + c.DTStart = &dtstart.Time + } + if dtend.Valid { + c.DTEnd = &dtend.Time + } + out = append(out, c) + } + return out, nil +} + +func (db *DB) GetCalendarObject(calendarID, uid string) (*CalendarObject, error) { + row := db.QueryRow(`SELECT id, calendar_id, uid, ical_enc, component_type, summary, dtstart, dtend, etag, created_at, updated_at + FROM calendar_objects WHERE calendar_id = ? AND uid = ?`, calendarID, uid) + var c CalendarObject + var componentType, summary sql.NullString + var dtstart, dtend sql.NullTime + err := row.Scan(&c.ID, &c.CalendarID, &c.UID, &c.ICalEnc, &componentType, &summary, &dtstart, &dtend, &c.ETag, &c.CreatedAt, &c.UpdatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get calendar object: %w", err) + } + c.ComponentType = componentType.String + c.Summary = summary.String + if dtstart.Valid { + c.DTStart = &dtstart.Time + } + if dtend.Valid { + c.DTEnd = &dtend.Time + } + return &c, nil +} + +func (db *DB) UpsertCalendarObject(c *CalendarObject) error { + existing, err := db.GetCalendarObject(c.CalendarID, c.UID) + if err == nil { + _, err := db.Exec(`UPDATE calendar_objects SET ical_enc = ?, component_type = ?, summary = ?, dtstart = ?, dtend = ?, etag = ?, updated_at = ? WHERE id = ?`, + c.ICalEnc, c.ComponentType, c.Summary, c.DTStart, c.DTEnd, c.ETag, time.Now().UTC(), existing.ID) + return err + } + _, err = db.Exec(`INSERT INTO calendar_objects (id, calendar_id, uid, ical_enc, component_type, summary, dtstart, dtend, etag) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + c.ID, c.CalendarID, c.UID, c.ICalEnc, c.ComponentType, c.Summary, c.DTStart, c.DTEnd, c.ETag) + return err +} + +func (db *DB) DeleteCalendarObject(calendarID, uid string) error { + _, err := db.Exec(`DELETE FROM calendar_objects WHERE calendar_id = ? AND uid = ?`, calendarID, uid) + return err +} + +// ── ManageSieve ─────────────────────────────────────────────────────────────── + +func (db *DB) ListSieveScripts(userID string) ([]SieveScript, error) { + rows, err := db.Query(`SELECT id, user_id, name, script_text, active, created_at, updated_at + FROM sieve_scripts WHERE user_id = ? ORDER BY name`, userID) + if err != nil { + return nil, fmt.Errorf("list sieve scripts: %w", err) + } + defer rows.Close() + var out []SieveScript + for rows.Next() { + var s SieveScript + if err := rows.Scan(&s.ID, &s.UserID, &s.Name, &s.ScriptText, &s.Active, &s.CreatedAt, &s.UpdatedAt); err != nil { + continue + } + out = append(out, s) + } + return out, nil +} + +func (db *DB) GetSieveScript(userID, name string) (*SieveScript, error) { + row := db.QueryRow(`SELECT id, user_id, name, script_text, active, created_at, updated_at + FROM sieve_scripts WHERE user_id = ? AND name = ?`, userID, name) + var s SieveScript + err := row.Scan(&s.ID, &s.UserID, &s.Name, &s.ScriptText, &s.Active, &s.CreatedAt, &s.UpdatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get sieve script: %w", err) + } + return &s, nil +} + +// GetActiveSieveScript returns the user's currently active script, if any — +// called on every inbound delivery, so kept as a single indexed lookup. +func (db *DB) GetActiveSieveScript(userID string) (*SieveScript, error) { + row := db.QueryRow(`SELECT id, user_id, name, script_text, active, created_at, updated_at + FROM sieve_scripts WHERE user_id = ? AND active = 1`, userID) + var s SieveScript + err := row.Scan(&s.ID, &s.UserID, &s.Name, &s.ScriptText, &s.Active, &s.CreatedAt, &s.UpdatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get active sieve script: %w", err) + } + return &s, nil +} + +func (db *DB) UpsertSieveScript(s *SieveScript) error { + existing, err := db.GetSieveScript(s.UserID, s.Name) + if err == nil { + _, err := db.Exec(`UPDATE sieve_scripts SET script_text = ?, updated_at = ? WHERE id = ?`, + s.ScriptText, time.Now().UTC(), existing.ID) + return err + } + _, err = db.Exec(`INSERT INTO sieve_scripts (id, user_id, name, script_text, active) VALUES (?, ?, ?, ?, 0)`, + s.ID, s.UserID, s.Name, s.ScriptText) + return err +} + +// SetActiveSieveScript activates the named script and deactivates every +// other script for the user — ManageSieve's SETACTIVE semantics require +// exactly zero or one active script per user at a time. +func (db *DB) SetActiveSieveScript(userID, name string) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(`UPDATE sieve_scripts SET active = 0 WHERE user_id = ?`, userID); err != nil { + return err + } + res, err := tx.Exec(`UPDATE sieve_scripts SET active = 1 WHERE user_id = ? AND name = ?`, userID, name) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + return ErrNotFound + } + return tx.Commit() +} + +func (db *DB) DeleteSieveScript(userID, name string) error { + _, err := db.Exec(`DELETE FROM sieve_scripts WHERE user_id = ? AND name = ?`, userID, name) + return err +} + +// ── TLS certs (ACME) ──────────────────────────────────────────────────────────── + +func (db *DB) GetTLSCert(domain string) (*TLSCert, error) { + row := db.QueryRow(`SELECT id, domain, cert_pem_enc, key_pem_enc, expires_at, acme_account_key_enc, created_at, updated_at + FROM tls_certs WHERE domain = ?`, domain) + var c TLSCert + err := row.Scan(&c.ID, &c.Domain, &c.CertPEMEnc, &c.KeyPEMEnc, &c.ExpiresAt, &c.ACMEAccountKeyEnc, &c.CreatedAt, &c.UpdatedAt) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get tls cert: %w", err) + } + return &c, nil +} + +// UpsertTLSCert creates or updates the stored certificate for a domain. +func (db *DB) UpsertTLSCert(c *TLSCert) error { + existing, err := db.GetTLSCert(c.Domain) + if err == nil { + _, err := db.Exec(`UPDATE tls_certs SET cert_pem_enc = ?, key_pem_enc = ?, expires_at = ?, updated_at = ? WHERE id = ?`, + c.CertPEMEnc, c.KeyPEMEnc, c.ExpiresAt, time.Now().UTC(), existing.ID) + return err + } + _, err = db.Exec(`INSERT INTO tls_certs (id, domain, cert_pem_enc, key_pem_enc, expires_at, acme_account_key_enc) VALUES (?, ?, ?, ?, ?, ?)`, + c.ID, c.Domain, c.CertPEMEnc, c.KeyPEMEnc, c.ExpiresAt, c.ACMEAccountKeyEnc) + return err +} + +// SetACMEAccountKey stores the ACME account key separately from cert +// issuance — the account key is created once and reused across renewals, +// while cert/key rotate every renewal. +func (db *DB) SetACMEAccountKey(domain string, keyEnc []byte) error { + existing, err := db.GetTLSCert(domain) + if err == nil { + _, err := db.Exec(`UPDATE tls_certs SET acme_account_key_enc = ? WHERE id = ?`, keyEnc, existing.ID) + return err + } + _, err = db.Exec(`INSERT INTO tls_certs (id, domain, acme_account_key_enc) VALUES (?, ?, ?)`, uuidNew(), domain, keyEnc) + return err +} + +// ── MFA ─────────────────────────────────────────────────────────────────────── + +// SetPendingTOTPSecret stores an encrypted TOTP secret WITHOUT enabling +// MFA yet — the user must confirm one valid code first (SetMFAEnabled), +// so a setup flow abandoned partway through never locks anyone out. +func (db *DB) SetPendingTOTPSecret(userID string, secretEnc []byte) error { + _, err := db.Exec(`UPDATE users SET totp_secret_enc = ? WHERE id = ?`, secretEnc, userID) + return err +} + +func (db *DB) SetMFAEnabled(userID string, enabled bool) error { + _, err := db.Exec(`UPDATE users SET mfa_enabled = ? WHERE id = ?`, enabled, userID) + return err +} + +func (db *DB) ClearTOTPSecret(userID string) error { + _, err := db.Exec(`UPDATE users SET mfa_enabled = 0, totp_secret_enc = NULL WHERE id = ?`, userID) + return err +} + +// ReplaceBackupCodes deletes any existing backup codes for the user and +// inserts a fresh set — called once at MFA confirm time; codes are shown to +// the user exactly once, matching how app passwords are handled. +func (db *DB) ReplaceBackupCodes(userID string, codeHashes []string) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(`DELETE FROM mfa_backup_codes WHERE user_id = ?`, userID); err != nil { + return err + } + for _, hash := range codeHashes { + if _, err := tx.Exec(`INSERT INTO mfa_backup_codes (id, user_id, code_hash) VALUES (?, ?, ?)`, uuidNew(), userID, hash); err != nil { + return err + } + } + return tx.Commit() +} + +// ConsumeBackupCode checks candidateCode against every unused backup code +// hash for the user and, on a match, marks that one used (one-time use) — +// returns true if a match was found and consumed. +func (db *DB) ConsumeBackupCode(userID, candidateHash string) (bool, error) { + rows, err := db.Query(`SELECT id, code_hash FROM mfa_backup_codes WHERE user_id = ? AND used_at IS NULL`, userID) + if err != nil { + return false, err + } + + var matchID string + for rows.Next() { + var id, hash string + if err := rows.Scan(&id, &hash); err != nil { + continue + } + if subtle.ConstantTimeCompare([]byte(hash), []byte(candidateHash)) == 1 { + matchID = id + break + } + } + // Close explicitly (not just deferred) before the UPDATE below — SQLite + // is capped to a single open connection (see db.Open), so an UPDATE + // issued while these still-open rows hold that one connection would + // deadlock waiting for a connection that can't free until Close() runs. + rows.Close() + + if matchID == "" { + return false, nil + } + if _, err := db.Exec(`UPDATE mfa_backup_codes SET used_at = ? WHERE id = ?`, time.Now().UTC(), matchID); err != nil { + return false, err + } + return true, nil +} + +func (db *DB) SetRecoveryEmail(userID, email string) error { + _, err := db.Exec(`UPDATE users SET recovery_email = ? WHERE id = ?`, email, userID) + return err +} + +// ── Outbound queue ─────────────────────────────────────────────────────────── + +// InsertOutboundQueueEntry enqueues a message for outbound delivery. +func (db *DB) InsertOutboundQueueEntry(e *OutboundQueueEntry) error { + _, err := db.Exec(` + INSERT INTO outbound_queue (id, user_id, from_address, to_address, eml_path, priority, next_attempt_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, e.ID, e.UserID, e.FromAddress, e.ToAddress, e.EMLPath, e.Priority, e.NextAttemptAt) + if err != nil { + return fmt.Errorf("insert outbound queue entry: %w", err) + } + return nil +} + +// DueOutboundEntries returns queue entries ready for a delivery attempt, +// ordered by priority (desc) then age (oldest first), up to limit rows. +func (db *DB) DueOutboundEntries(maxAttempts, limit int) ([]OutboundQueueEntry, error) { + rows, err := db.Query(` + SELECT id, user_id, from_address, to_address, eml_path, priority, attempts, last_error, next_attempt_at, created_at + FROM outbound_queue + WHERE next_attempt_at <= ? AND attempts < ? + ORDER BY priority DESC, created_at ASC + LIMIT ? + `, time.Now().UTC(), maxAttempts, limit) + if err != nil { + return nil, fmt.Errorf("query due outbound entries: %w", err) + } + defer rows.Close() + + var entries []OutboundQueueEntry + for rows.Next() { + var e OutboundQueueEntry + var lastError sql.NullString + if err := rows.Scan(&e.ID, &e.UserID, &e.FromAddress, &e.ToAddress, &e.EMLPath, + &e.Priority, &e.Attempts, &lastError, &e.NextAttemptAt, &e.CreatedAt); err != nil { + continue + } + e.LastError = lastError.String + entries = append(entries, e) + } + return entries, nil +} + +// DeleteOutboundEntry removes a queue entry after successful delivery or +// permanent failure (bounce sent). +func (db *DB) DeleteOutboundEntry(id string) error { + _, err := db.Exec(`DELETE FROM outbound_queue WHERE id = ?`, id) + return err +} + +// RetryOutboundEntry records a failed attempt and schedules the next retry. +func (db *DB) RetryOutboundEntry(id string, nextAttempt time.Time, lastError string) error { + _, err := db.Exec(` + UPDATE outbound_queue SET attempts = attempts + 1, next_attempt_at = ?, last_error = ? + WHERE id = ? + `, nextAttempt, lastError, id) + return err +} + +// PermanentlyFailedEntries returns entries that have exhausted their retry +// attempts — the caller should bounce these and then delete them. +func (db *DB) PermanentlyFailedEntries(maxAttempts int) ([]OutboundQueueEntry, error) { + rows, err := db.Query(` + SELECT id, user_id, from_address, to_address, eml_path, priority, attempts, last_error, next_attempt_at, created_at + FROM outbound_queue WHERE attempts >= ? + `, maxAttempts) + if err != nil { + return nil, fmt.Errorf("query permanently failed entries: %w", err) + } + defer rows.Close() + + var entries []OutboundQueueEntry + for rows.Next() { + var e OutboundQueueEntry + var lastError sql.NullString + if err := rows.Scan(&e.ID, &e.UserID, &e.FromAddress, &e.ToAddress, &e.EMLPath, + &e.Priority, &e.Attempts, &lastError, &e.NextAttemptAt, &e.CreatedAt); err != nil { + continue + } + e.LastError = lastError.String + entries = append(entries, e) + } + return entries, nil +} + +// LookupDomainByName is a convenience alias used by the DKIM signer to find +// the sending domain's key material without also needing tenant info. +func (db *DB) LookupDomainByName(domain string) (*Domain, error) { + d, _, err := db.LookupDomain(domain) + return d, err +} diff --git a/internal/dkim/keys.go b/internal/dkim/keys.go new file mode 100644 index 0000000..7a9ed16 --- /dev/null +++ b/internal/dkim/keys.go @@ -0,0 +1,92 @@ +// Package dkim implements DKIM (RFC 6376) signing for outbound mail using +// only stdlib crypto — no third-party DKIM library. Verification of inbound +// DKIM signatures is added in Phase 4's security pipeline. +package dkim + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" +) + +// KeyPair holds a freshly generated DKIM signing key, both as PEM (for +// encrypted storage) and the DNS TXT record value the operator must publish. +type KeyPair struct { + PrivateKeyPEM []byte // PKCS#1 PEM — store encrypted in domains.dkim_private_key_enc + DNSRecordValue string // paste into: {selector}._domainkey.{domain} TXT record +} + +// GenerateKeyPair creates a new RSA-2048 DKIM key pair. RSA-2048 is used +// (rather than Ed25519) because it has universal support across mail +// receivers — Ed25519 DKIM (RFC 8463) support is not yet ubiquitous. +func GenerateKeyPair() (*KeyPair, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, fmt.Errorf("generating RSA key: %w", err) + } + + privDER := x509.MarshalPKCS1PrivateKey(priv) + privPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privDER, + }) + + pubDER, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + return nil, fmt.Errorf("marshaling public key: %w", err) + } + pubB64 := base64.StdEncoding.EncodeToString(pubDER) + + dnsValue := fmt.Sprintf("v=DKIM1; k=rsa; p=%s", pubB64) + + return &KeyPair{ + PrivateKeyPEM: privPEM, + DNSRecordValue: dnsValue, + }, nil +} + +// ParsePrivateKey decodes a PEM-encoded RSA private key (as produced by +// GenerateKeyPair, after decryption from storage). +func ParsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("no PEM block found") + } + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing RSA private key: %w", err) + } + return key, nil +} + +// ExtractSignatureInfo pulls the signing domain and selector out of a +// message's DKIM-Signature header, without doing any verification — the +// caller uses this to know which DNS TXT record to fetch before calling +// Verify. Returns found=false if no DKIM-Signature header is present. +func ExtractSignatureInfo(raw []byte) (domain, selector string, found bool) { + headers, _ := splitMessage(raw) + headerMap := parseHeaders(headers) + sigHeader, ok := headerMap["dkim-signature"] + if !ok { + return "", "", false + } + tags := parseDKIMTags(sigHeader) + domain = tags["d"] + selector = tags["s"] + return domain, selector, domain != "" && selector != "" +} + +// ParseDNSPublicKey decodes the "p=" tag value from a DKIM DNS TXT record +// (as published by GenerateKeyPair's DNSRecordValue, or any RFC 6376 +// compliant record) into the raw public key DER bytes Verify expects. +func ParseDNSPublicKey(txtRecord string) ([]byte, error) { + tags := parseDKIMTags(txtRecord) + pValue, ok := tags["p"] + if !ok || pValue == "" { + return nil, fmt.Errorf("no p= tag found in DNS record") + } + return base64.StdEncoding.DecodeString(pValue) +} diff --git a/internal/dkim/sign.go b/internal/dkim/sign.go new file mode 100644 index 0000000..6513128 --- /dev/null +++ b/internal/dkim/sign.go @@ -0,0 +1,195 @@ +package dkim + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "fmt" + "regexp" + "strings" + "time" +) + +// signedHeaders is the fixed set of headers we sign, in order, when present. +// Keeping this list small and stable avoids the classic DKIM pitfall of +// signing headers that get legitimately rewritten in transit (Received, etc). +var signedHeaders = []string{"from", "to", "subject", "date", "message-id"} + +// Sign adds a DKIM-Signature header to raw using relaxed/relaxed +// canonicalization and RSA-SHA256, per RFC 6376. Returns the message with +// the DKIM-Signature header prepended. +func Sign(privateKeyPEM []byte, domain, selector string, raw []byte) ([]byte, error) { + key, err := ParsePrivateKey(privateKeyPEM) + if err != nil { + return nil, err + } + + headers, body := splitMessage(raw) + bodyCanon := canonicalizeBodyRelaxed(body) + bodyHash := sha256.Sum256(bodyCanon) + bodyHashB64 := base64.StdEncoding.EncodeToString(bodyHash[:]) + + headerMap := parseHeaders(headers) + + var presentSigned []string + for _, h := range signedHeaders { + if _, ok := headerMap[h]; ok { + presentSigned = append(presentSigned, h) + } + } + if len(presentSigned) == 0 { + return nil, fmt.Errorf("no signable headers present in message") + } + + // Build the DKIM-Signature header with an empty b= tag first — this + // unsigned version is itself included (relaxed-canonicalized) in what we + // sign, per RFC 6376 §3.7. + dkimHeaderTemplate := buildDKIMHeader(domain, selector, presentSigned, bodyHashB64, "") + + signInput := canonicalizeHeadersRelaxed(headerMap, presentSigned) + signInput = append(signInput, canonicalizeHeaderRelaxed("dkim-signature", dkimHeaderTemplate)...) + // Per spec, the DKIM-Signature header itself is canonicalized WITHOUT a + // trailing CRLF when it's the last (signed) header being hashed. + signInput = bytes.TrimSuffix(signInput, []byte("\r\n")) + + hashed := sha256.Sum256(signInput) + signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hashed[:]) + if err != nil { + return nil, fmt.Errorf("signing: %w", err) + } + sigB64 := base64.StdEncoding.EncodeToString(signature) + + finalHeader := buildDKIMHeader(domain, selector, presentSigned, bodyHashB64, sigB64) + + var out bytes.Buffer + out.WriteString("DKIM-Signature: ") + out.WriteString(finalHeader) + out.WriteString("\r\n") + out.Write(headers) + out.Write(body) + + return out.Bytes(), nil +} + +func buildDKIMHeader(domain, selector string, signedHdrs []string, bodyHashB64, sigB64 string) string { + return fmt.Sprintf( + "v=1; a=rsa-sha256; c=relaxed/relaxed; d=%s; s=%s; t=%d; h=%s; bh=%s; b=%s", + domain, selector, time.Now().Unix(), strings.Join(signedHdrs, ":"), bodyHashB64, sigB64, + ) +} + +// splitMessage separates the raw RFC 5322 message into its header block +// (including the trailing blank line's CRLF) and body. +func splitMessage(raw []byte) (headers, body []byte) { + sep := []byte("\r\n\r\n") + idx := bytes.Index(raw, sep) + if idx == -1 { + // Tolerate bare-LF input (shouldn't happen from our own DATA reader, + // which always produces CRLF, but be defensive). + sep = []byte("\n\n") + idx = bytes.Index(raw, sep) + if idx == -1 { + return raw, nil + } + } + return raw[:idx+len(sep)], raw[idx+len(sep):] +} + +// parseHeaders builds a lowercase-name -> raw-value-with-original-case map, +// unfolding continuation lines (RFC 5322 §2.2.3). +func parseHeaders(headerBlock []byte) map[string]string { + result := map[string]string{} + lines := strings.Split(string(headerBlock), "\r\n") + + var currentName, currentValue string + flush := func() { + if currentName != "" { + result[strings.ToLower(currentName)] = currentValue + } + } + + for _, line := range lines { + if line == "" { + continue + } + if (strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")) && currentName != "" { + currentValue += " " + strings.TrimSpace(line) + continue + } + flush() + name, value, found := strings.Cut(line, ":") + if !found { + currentName = "" + continue + } + currentName = strings.TrimSpace(name) + currentValue = strings.TrimSpace(value) + } + flush() + return result +} + +// canonicalizeHeadersRelaxed builds the signed-header block per RFC 6376 +// §3.4.2: lowercase header name, unfold, collapse WSP runs to single space, +// trim trailing WSP on the value, each header terminated with CRLF, in the +// exact order listed by names. +func canonicalizeHeadersRelaxed(headerMap map[string]string, names []string) []byte { + var buf bytes.Buffer + for _, name := range names { + value, ok := headerMap[name] + if !ok { + continue + } + buf.Write(canonicalizeHeaderRelaxed(name, value)) + } + return buf.Bytes() +} + +func canonicalizeHeaderRelaxed(name, value string) []byte { + name = strings.ToLower(strings.TrimSpace(name)) + value = collapseWSP(strings.TrimSpace(value)) + return []byte(name + ":" + value + "\r\n") +} + +var wspRunRE = regexp.MustCompile(`[ \t]+`) + +func collapseWSP(s string) string { + return wspRunRE.ReplaceAllString(s, " ") +} + +// canonicalizeBodyRelaxed implements RFC 6376 §3.4.4: reduce WSP sequences +// within a line to a single space, remove trailing WSP from each line, +// remove trailing empty lines (but keep exactly one CRLF if the body is +// non-empty after trimming). +func canonicalizeBodyRelaxed(body []byte) []byte { + if len(body) == 0 { + return []byte("") + } + + lines := bytes.Split(body, []byte("\r\n")) + for i, line := range lines { + line = wspRunRE.ReplaceAll(line, []byte(" ")) + lines[i] = bytes.TrimRight(line, " \t") + } + + // Remove trailing empty lines. + end := len(lines) + for end > 0 && len(lines[end-1]) == 0 { + end-- + } + lines = lines[:end] + + if len(lines) == 0 { + return []byte("") + } + + var buf bytes.Buffer + for _, line := range lines { + buf.Write(line) + buf.WriteString("\r\n") + } + return buf.Bytes() +} diff --git a/internal/dkim/verify.go b/internal/dkim/verify.go new file mode 100644 index 0000000..680fc72 --- /dev/null +++ b/internal/dkim/verify.go @@ -0,0 +1,108 @@ +package dkim + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "fmt" + "strings" +) + +// Verify checks a signed message's DKIM-Signature header against the given +// public key (as would be fetched from DNS in Phase 4's inbound pipeline). +// This lean version only handles rsa-sha256/relaxed-relaxed — the exact +// profile Sign() produces — since its purpose here is to prove the signer is +// correct. Phase 4 will build a fuller verifier (multiple algorithms, +// simple/simple and mixed canonicalization) for arbitrary inbound mail. +func Verify(publicKeyDER []byte, raw []byte) error { + headers, body := splitMessage(raw) + headerMap := parseHeaders(headers) + + dkimHeaderValue, ok := headerMap["dkim-signature"] + if !ok { + return fmt.Errorf("no DKIM-Signature header present") + } + + tags := parseDKIMTags(dkimHeaderValue) + if tags["a"] != "rsa-sha256" { + return fmt.Errorf("unsupported algorithm: %s", tags["a"]) + } + if tags["c"] != "relaxed/relaxed" { + return fmt.Errorf("unsupported canonicalization: %s", tags["c"]) + } + + // Verify body hash. + bodyCanon := canonicalizeBodyRelaxed(body) + bodyHash := sha256.Sum256(bodyCanon) + expectedBH := base64.StdEncoding.EncodeToString(bodyHash[:]) + if tags["bh"] != expectedBH { + return fmt.Errorf("body hash mismatch: signature claims %s, computed %s", tags["bh"], expectedBH) + } + + signedHdrNames := strings.Split(tags["h"], ":") + + // Rebuild the exact signing input: canonicalized signed headers, then the + // DKIM-Signature header itself with b= emptied, no trailing CRLF. + signInput := canonicalizeHeadersRelaxed(headerMap, signedHdrNames) + + dkimHeaderNoB := replaceDKIMTag(dkimHeaderValue, "b", "") + signInput = append(signInput, canonicalizeHeaderRelaxed("dkim-signature", dkimHeaderNoB)...) + signInput = trimTrailingCRLF(signInput) + + sigBytes, err := base64.StdEncoding.DecodeString(tags["b"]) + if err != nil { + return fmt.Errorf("decoding signature: %w", err) + } + + pubAny, err := x509.ParsePKIXPublicKey(publicKeyDER) + if err != nil { + return fmt.Errorf("parsing public key: %w", err) + } + pubKey, ok := pubAny.(*rsa.PublicKey) + if !ok { + return fmt.Errorf("public key is not RSA") + } + + hashed := sha256.Sum256(signInput) + if err := rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], sigBytes); err != nil { + return fmt.Errorf("signature verification failed: %w", err) + } + + return nil +} + +func parseDKIMTags(header string) map[string]string { + tags := map[string]string{} + for _, part := range strings.Split(header, ";") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + name, value, found := strings.Cut(part, "=") + if !found { + continue + } + tags[strings.TrimSpace(name)] = strings.TrimSpace(value) + } + return tags +} + +func replaceDKIMTag(header, tag, newValue string) string { + parts := strings.Split(header, ";") + for i, part := range parts { + trimmed := strings.TrimSpace(part) + if strings.HasPrefix(trimmed, tag+"=") { + parts[i] = " " + tag + "=" + newValue + } + } + return strings.Join(parts, ";") +} + +func trimTrailingCRLF(b []byte) []byte { + for len(b) >= 2 && b[len(b)-2] == '\r' && b[len(b)-1] == '\n' { + return b[:len(b)-2] + } + return b +} diff --git a/internal/ical/ical.go b/internal/ical/ical.go new file mode 100644 index 0000000..3cbde91 --- /dev/null +++ b/internal/ical/ical.go @@ -0,0 +1,150 @@ +// Package ical implements a minimal RFC 5545 iCalendar parser/builder — just +// VEVENT with the fields CalDAV needs: UID, SUMMARY, DTSTART, DTEND, +// DESCRIPTION, LOCATION. Not full RFC 5545 (no VTODO/VJOURNAL/VALARM, no +// RRULE recurrence) — enough for real calendar clients to create, fetch, and +// list single events, with recurrence and other component types as natural +// next additions once client compatibility testing calls for them. +package ical + +import ( + "fmt" + "strings" + "time" +) + +const icalTimeLayout = "20060102T150405Z" + +type Event struct { + UID string + Summary string + Description string + Location string + DTStart time.Time + DTEnd time.Time +} + +// Parse reads a VCALENDAR containing one VEVENT. +func Parse(data string) (*Event, error) { + lines := unfold(data) + e := &Event{} + inEvent := false + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + upper := strings.ToUpper(line) + switch { + case upper == "BEGIN:VEVENT": + inEvent = true + continue + case upper == "END:VEVENT": + inEvent = false + continue + } + if !inEvent { + continue + } + + name, value, found := splitProperty(line) + if !found { + continue + } + switch strings.ToUpper(name) { + case "UID": + e.UID = value + case "SUMMARY": + e.Summary = unescape(value) + case "DESCRIPTION": + e.Description = unescape(value) + case "LOCATION": + e.Location = unescape(value) + case "DTSTART": + if t, err := time.Parse(icalTimeLayout, value); err == nil { + e.DTStart = t + } + case "DTEND": + if t, err := time.Parse(icalTimeLayout, value); err == nil { + e.DTEnd = t + } + } + } + + if e.UID == "" { + return nil, fmt.Errorf("ical missing required UID property") + } + return e, nil +} + +// Build renders an Event back into a full VCALENDAR/VEVENT block, CRLF line +// endings per spec. +func (e *Event) Build() string { + var b strings.Builder + b.WriteString("BEGIN:VCALENDAR\r\n") + b.WriteString("VERSION:2.0\r\n") + b.WriteString("PRODID:-//GoMail//CalDAV//EN\r\n") + b.WriteString("BEGIN:VEVENT\r\n") + b.WriteString("UID:" + e.UID + "\r\n") + if !e.DTStart.IsZero() { + b.WriteString("DTSTART:" + e.DTStart.UTC().Format(icalTimeLayout) + "\r\n") + } + if !e.DTEnd.IsZero() { + b.WriteString("DTEND:" + e.DTEnd.UTC().Format(icalTimeLayout) + "\r\n") + } + if e.Summary != "" { + b.WriteString("SUMMARY:" + escape(e.Summary) + "\r\n") + } + if e.Description != "" { + b.WriteString("DESCRIPTION:" + escape(e.Description) + "\r\n") + } + if e.Location != "" { + b.WriteString("LOCATION:" + escape(e.Location) + "\r\n") + } + b.WriteString("END:VEVENT\r\n") + b.WriteString("END:VCALENDAR\r\n") + return b.String() +} + +func splitProperty(line string) (name, value string, found bool) { + colonIdx := strings.Index(line, ":") + if colonIdx == -1 { + return "", "", false + } + namePart := line[:colonIdx] + value = line[colonIdx+1:] + if semiIdx := strings.Index(namePart, ";"); semiIdx != -1 { + namePart = namePart[:semiIdx] + } + return namePart, value, true +} + +// unfold reverses RFC 5545 §3.1 line folding, same rule as vCard's. +func unfold(data string) []string { + raw := strings.Split(strings.ReplaceAll(data, "\r\n", "\n"), "\n") + var out []string + for _, line := range raw { + if len(line) > 0 && (line[0] == ' ' || line[0] == '\t') && len(out) > 0 { + out[len(out)-1] += line[1:] + } else { + out = append(out, line) + } + } + return out +} + +func escape(s string) string { + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, ",", "\\,") + s = strings.ReplaceAll(s, ";", "\\;") + s = strings.ReplaceAll(s, "\n", "\\n") + return s +} + +func unescape(s string) string { + s = strings.ReplaceAll(s, "\\n", "\n") + s = strings.ReplaceAll(s, "\\,", ",") + s = strings.ReplaceAll(s, "\\;", ";") + s = strings.ReplaceAll(s, "\\\\", "\\") + return s +} diff --git a/internal/ical/ical_fuzz_test.go b/internal/ical/ical_fuzz_test.go new file mode 100644 index 0000000..6585812 --- /dev/null +++ b/internal/ical/ical_fuzz_test.go @@ -0,0 +1,23 @@ +package ical + +import "testing" + +func FuzzParse(f *testing.F) { + f.Add("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-1\r\nDTSTART:20260101T120000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n") + f.Add("BEGIN:VEVENT\nUID:no-crlf\nEND:VEVENT\n") + f.Add("BEGIN:VEVENT\r\nUID:folded\r\nDESCRIPTION:line one\r\n continued\r\nEND:VEVENT\r\n") + f.Add("") + f.Add("BEGIN:VEVENT\r\nEND:VEVENT\r\n") + f.Add("not an ical at all") + f.Add("BEGIN:VEVENT\r\nDTSTART:not-a-real-date\r\nUID:x\r\nEND:VEVENT\r\n") + f.Add("BEGIN:VEVENT\r\n:\r\nUID:x\r\nEND:VEVENT\r\n") + + f.Fuzz(func(t *testing.T, data string) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("Parse panicked on input %q: %v", data, r) + } + }() + Parse(data) + }) +} diff --git a/internal/imap/commands.go b/internal/imap/commands.go new file mode 100644 index 0000000..86e9218 --- /dev/null +++ b/internal/imap/commands.go @@ -0,0 +1,518 @@ +package imap + +import ( + "fmt" + "strconv" + "strings" + + "gomail/internal/db" +) + +func (s *session) cmdCapability(tag string) { + caps := "CAPABILITY IMAP4rev1" + if !s.tlsActive { + caps += " STARTTLS LOGINDISABLED" + } else { + caps += " AUTH=LOGIN" + } + s.untagged(caps) + s.tagged(tag, "OK CAPABILITY completed") +} + +func (s *session) cmdStartTLS(tag string) { + if s.tlsActive { + s.tagged(tag, "BAD TLS already active") + return + } + s.tagged(tag, "OK begin TLS negotiation now") + if err := s.upgradeTLS(s.server.tlsConf); err != nil { + return // connection is likely unusable now; caller's read loop will error out and close + } + s.tlsActive = true +} + +func (s *session) cmdLogin(tag string, args []string) { + if !s.tlsActive { + s.tagged(tag, "NO LOGIN over plaintext refused — use STARTTLS or connect on the implicit-TLS port") + return + } + + // Checked before attempting any credential verification — same + // rationale as smtp.session.handleAuth's authLimiter check. + ip := connHost(s.conn.RemoteAddr()) + if !s.server.authLimiter.Allow(ip) { + s.tagged(tag, "NO too many authentication attempts, try again later") + return + } + + if len(args) < 2 { + s.tagged(tag, "BAD LOGIN requires username and password") + return + } + username, password := args[0], args[1] + + if !s.authenticateUser(username, password) { + s.tagged(tag, "NO LOGIN failed") + return + } + s.tagged(tag, "OK LOGIN completed") +} + +func (s *session) cmdSelectExamine(tag string, args []string, readWrite bool) { + if !s.requireAuthenticated(tag) { + return + } + if len(args) < 1 { + s.tagged(tag, "BAD SELECT/EXAMINE requires a mailbox name") + return + } + mailbox := args[0] + + entries, err := s.server.database.ListMailboxEntries(s.user.ID, mailbox) + if err != nil { + s.tagged(tag, "NO SELECT failed: "+err.Error()) + return + } + + s.mailbox = mailbox + s.entries = entries + s.readOnly = !readWrite + s.state = stateSelected + + unseen := 0 + nextUID := 1 + for i, e := range entries { + if !strings.Contains(e.Flags, "\\Seen") && unseen == 0 { + s.untagged(fmt.Sprintf("OK [UNSEEN %d] first unseen", i+1)) + unseen = i + 1 + } + if e.UID >= nextUID { + nextUID = e.UID + 1 + } + } + + s.untagged(fmt.Sprintf("%d EXISTS", len(entries))) + s.untagged("0 RECENT") + s.untagged("FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)") + s.untagged("OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)] Limited") + s.untagged("OK [UIDVALIDITY 1] UIDs valid") + s.untagged(fmt.Sprintf("OK [UIDNEXT %d] Predicted next UID", nextUID)) + + if readWrite { + s.tagged(tag, "OK [READ-WRITE] SELECT completed") + } else { + s.tagged(tag, "OK [READ-ONLY] EXAMINE completed") + } +} + +func (s *session) cmdList(tag string, args []string) { + if !s.requireAuthenticated(tag) { + return + } + // args: reference-name mailbox-pattern — we ignore hierarchy and just + // list every mailbox the user has, since GoMail's folder model is flat + // (no nested folders yet). A "%"/"*" wildcard pattern matches everything + // in this simplified model. + names, err := s.server.database.ListMailboxNames(s.user.ID) + if err != nil { + s.tagged(tag, "NO LIST failed: "+err.Error()) + return + } + for _, name := range names { + s.untagged(fmt.Sprintf(`LIST () "/" %s`, quoteIfNeeded(name))) + } + s.tagged(tag, "OK LIST completed") +} + +func (s *session) cmdClose(tag string) { + if !s.requireSelected(tag) { + return + } + s.expungeDeleted() + s.mailbox = "" + s.entries = nil + s.state = stateAuthenticated + s.tagged(tag, "OK CLOSE completed") +} + +func (s *session) cmdExpunge(tag string) { + if !s.requireSelected(tag) { + return + } + if s.readOnly { + s.tagged(tag, "NO mailbox is read-only") + return + } + removed := s.expungeDeleted() + s.tagged(tag, fmt.Sprintf("OK EXPUNGE completed (%d removed)", removed)) +} + +// expungeDeleted removes every \Deleted-flagged message from storage and the +// index, sends the required untagged "N EXPUNGE" responses (in descending +// sequence order, per RFC 3501 §6.4.3 — removing from the end first keeps +// earlier sequence numbers stable for any remaining EXPUNGE responses in the +// same batch), and refreshes the in-memory snapshot. +func (s *session) expungeDeleted() int { + var kept []db.MailboxEntry + var removedSeqs []int + + for i, e := range s.entries { + if strings.Contains(e.Flags, "\\Deleted") { + removedSeqs = append(removedSeqs, i+1) + s.server.database.DeleteMailboxEntry(e.ID) + // Best-effort file removal — the DB row is the source of truth for + // "does this message exist"; a leftover encrypted file with no + // index row is inert. + } else { + kept = append(kept, e) + } + } + + for i := len(removedSeqs) - 1; i >= 0; i-- { + s.untagged(fmt.Sprintf("%d EXPUNGE", removedSeqs[i])) + } + + s.entries = kept + return len(removedSeqs) +} + +func (s *session) cmdUID(tag string, args []string) { + if len(args) < 1 { + s.tagged(tag, "BAD UID requires a subcommand") + return + } + sub := strings.ToUpper(args[0]) + rest := args[1:] + + switch sub { + case "FETCH": + s.cmdFetch(tag, rest, true) + case "STORE": + s.cmdStore(tag, rest, true) + case "SEARCH": + s.cmdSearch(tag, rest, true) + default: + s.tagged(tag, "BAD UID subcommand not recognized") + } +} + +// ── FETCH ───────────────────────────────────────────────────────────────────── + +func (s *session) cmdFetch(tag string, args []string, byUID bool) { + if !s.requireSelected(tag) { + return + } + if len(args) < 2 { + s.tagged(tag, "BAD FETCH requires a sequence-set and item list") + return + } + + targets := s.resolveSequenceSet(args[0], byUID) + items := expandFetchItems(args[1]) + + for _, idx := range targets { + entry := s.entries[idx] + s.sendFetchResponse(idx+1, entry, items) + } + s.tagged(tag, "OK FETCH completed") +} + +func expandFetchItems(token string) []string { + var items []string + if isList(token) { + items = splitList(token) + } else { + items = []string{token} + } + var expanded []string + for _, item := range items { + switch strings.ToUpper(item) { + case "FAST": + expanded = append(expanded, "FLAGS", "INTERNALDATE", "RFC822.SIZE") + case "ALL": + expanded = append(expanded, "FLAGS", "INTERNALDATE", "RFC822.SIZE") + case "FULL": + expanded = append(expanded, "FLAGS", "INTERNALDATE", "RFC822.SIZE", "BODY[]") + default: + expanded = append(expanded, item) + } + } + return expanded +} + +func (s *session) sendFetchResponse(seq int, entry db.MailboxEntry, items []string) { + var parts []string + markSeen := false + + for _, item := range items { + upper := strings.ToUpper(item) + switch { + case upper == "FLAGS": + parts = append(parts, "FLAGS ("+flagsToIMAP(entry.Flags)+")") + case upper == "UID": + parts = append(parts, fmt.Sprintf("UID %d", entry.UID)) + case upper == "RFC822.SIZE": + parts = append(parts, fmt.Sprintf("RFC822.SIZE %d", entry.SizeBytes)) + case upper == "INTERNALDATE": + parts = append(parts, fmt.Sprintf(`INTERNALDATE "%s"`, entry.InternalDate.Format("02-Jan-2006 15:04:05 -0700"))) + case upper == "BODY[]" || upper == "RFC822": + raw, err := s.server.store.Read(entry.EMLPath) + if err == nil { + parts = append(parts, fmt.Sprintf("BODY[] {%d}\r\n%s", len(raw), raw)) + markSeen = true + } + case upper == "BODY.PEEK[]": + raw, err := s.server.store.Read(entry.EMLPath) + if err == nil { + parts = append(parts, fmt.Sprintf("BODY[] {%d}\r\n%s", len(raw), raw)) + } + case upper == "BODY[HEADER]" || upper == "RFC822.HEADER" || upper == "BODY.PEEK[HEADER]": + raw, err := s.server.store.Read(entry.EMLPath) + if err == nil { + headers := extractHeaders(raw) + parts = append(parts, fmt.Sprintf("BODY[HEADER] {%d}\r\n%s", len(headers), headers)) + if upper == "RFC822.HEADER" { + markSeen = true + } + } + } + } + + if markSeen && !strings.Contains(entry.Flags, "\\Seen") { + newFlags := addFlag(entry.Flags, "\\Seen") + s.server.database.UpdateMailboxFlags(entry.ID, newFlags) + for i := range s.entries { + if s.entries[i].ID == entry.ID { + s.entries[i].Flags = newFlags + } + } + } + + s.untagged(fmt.Sprintf("%d FETCH (%s)", seq, strings.Join(parts, " "))) +} + +func extractHeaders(raw []byte) []byte { + sep := []byte("\r\n\r\n") + if idx := indexOf(raw, sep); idx >= 0 { + return raw[:idx+2] + } + return raw +} + +func indexOf(haystack, needle []byte) int { + for i := 0; i+len(needle) <= len(haystack); i++ { + match := true + for j := range needle { + if haystack[i+j] != needle[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +// ── STORE ───────────────────────────────────────────────────────────────────── + +func (s *session) cmdStore(tag string, args []string, byUID bool) { + if !s.requireSelected(tag) { + return + } + if s.readOnly { + s.tagged(tag, "NO mailbox is read-only") + return + } + if len(args) < 3 { + s.tagged(tag, "BAD STORE requires sequence-set, item, and flag list") + return + } + + targets := s.resolveSequenceSet(args[0], byUID) + action := strings.ToUpper(args[1]) + newFlags := splitList(args[2]) + if len(newFlags) == 0 { + newFlags = args[2:] + } + + silent := strings.Contains(action, ".SILENT") + + for _, idx := range targets { + entry := &s.entries[idx] + switch { + case strings.HasPrefix(action, "+FLAGS"): + for _, f := range newFlags { + entry.Flags = addFlag(entry.Flags, f) + } + case strings.HasPrefix(action, "-FLAGS"): + for _, f := range newFlags { + entry.Flags = removeFlag(entry.Flags, f) + } + case strings.HasPrefix(action, "FLAGS"): + entry.Flags = strings.Join(newFlags, " ") + default: + continue + } + s.server.database.UpdateMailboxFlags(entry.ID, entry.Flags) + + if !silent { + s.untagged(fmt.Sprintf("%d FETCH (FLAGS (%s))", idx+1, flagsToIMAP(entry.Flags))) + } + } + + s.tagged(tag, "OK STORE completed") +} + +func addFlag(flags, flag string) string { + if strings.Contains(flags, flag) { + return flags + } + if flags == "" { + return flag + } + return flags + " " + flag +} + +func removeFlag(flags, flag string) string { + parts := strings.Fields(flags) + var out []string + for _, p := range parts { + if p != flag { + out = append(out, p) + } + } + return strings.Join(out, " ") +} + +func flagsToIMAP(flags string) string { + return flags // stored representation already matches IMAP flag syntax +} + +// ── SEARCH ──────────────────────────────────────────────────────────────────── + +func (s *session) cmdSearch(tag string, args []string, byUID bool) { + if !s.requireSelected(tag) { + return + } + if len(args) == 0 { + s.tagged(tag, "BAD SEARCH requires criteria") + return + } + + var matches []int + for i, entry := range s.entries { + if matchesSearch(entry, args) { + if byUID { + matches = append(matches, entry.UID) + } else { + matches = append(matches, i+1) + } + } + } + + strs := make([]string, len(matches)) + for i, m := range matches { + strs[i] = strconv.Itoa(m) + } + s.untagged("SEARCH " + strings.Join(strs, " ")) + s.tagged(tag, "OK SEARCH completed") +} + +// matchesSearch supports a pragmatic subset: ALL, UNSEEN, SEEN, ANSWERED, +// DELETED, FLAGGED, plus one-shot FROM/SUBJECT substring matching (checked +// against the flags string / a lightweight header scan). Full IMAP SEARCH +// grammar (nested boolean groups, date ranges, OR) is deferred. +func matchesSearch(entry db.MailboxEntry, criteria []string) bool { + for i := 0; i < len(criteria); i++ { + switch strings.ToUpper(criteria[i]) { + case "ALL": + continue + case "UNSEEN": + if strings.Contains(entry.Flags, "\\Seen") { + return false + } + case "SEEN": + if !strings.Contains(entry.Flags, "\\Seen") { + return false + } + case "ANSWERED": + if !strings.Contains(entry.Flags, "\\Answered") { + return false + } + case "DELETED": + if !strings.Contains(entry.Flags, "\\Deleted") { + return false + } + case "FLAGGED": + if !strings.Contains(entry.Flags, "\\Flagged") { + return false + } + } + } + return true +} + +// ── Sequence set resolution ──────────────────────────────────────────────────── + +// resolveSequenceSet parses "1", "1:3", "1,3,5", "1:*" (sequence numbers) or +// the equivalent for UIDs when byUID is true, and returns 0-based indexes +// into s.entries. +func (s *session) resolveSequenceSet(spec string, byUID bool) []int { + var result []int + seen := map[int]bool{} + + for _, part := range strings.Split(spec, ",") { + var lo, hi int + if strings.Contains(part, ":") { + bounds := strings.SplitN(part, ":", 2) + lo = parseSeqNum(bounds[0], byUID, s.entries) + hi = parseSeqNum(bounds[1], byUID, s.entries) + if lo > hi { + lo, hi = hi, lo + } + } else { + lo = parseSeqNum(part, byUID, s.entries) + hi = lo + } + + for i, e := range s.entries { + var val int + if byUID { + val = e.UID + } else { + val = i + 1 + } + if val >= lo && val <= hi && !seen[i] { + seen[i] = true + result = append(result, i) + } + } + } + return result +} + +func parseSeqNum(s string, byUID bool, entries []db.MailboxEntry) int { + if s == "*" { + if len(entries) == 0 { + return 0 + } + if byUID { + return entries[len(entries)-1].UID + } + return len(entries) + } + n, err := strconv.Atoi(s) + if err != nil { + return 0 + } + return n +} + +func quoteIfNeeded(name string) string { + if strings.ContainsAny(name, " \t()\"") { + return `"` + strings.ReplaceAll(name, `"`, `\"`) + `"` + } + return name +} diff --git a/internal/imap/parser.go b/internal/imap/parser.go new file mode 100644 index 0000000..f59d018 --- /dev/null +++ b/internal/imap/parser.go @@ -0,0 +1,77 @@ +package imap + +import "strings" + +// tokenize splits an IMAP command line into space-separated tokens, treating +// "quoted strings" and (parenthesized lists) as single tokens (lists keep +// their outer parens so command handlers can recognize and further split +// them). Literal syntax ({n}\r\n) is not handled here — see session.go's +// readCommand, which handles literals as a pre-pass before tokenizing since +// they require reading raw bytes off the connection, not just string scanning. +func tokenize(line string) []string { + var tokens []string + i, n := 0, len(line) + + for i < n { + for i < n && (line[i] == ' ' || line[i] == '\t') { + i++ + } + if i >= n { + break + } + + switch line[i] { + case '"': + j := i + 1 + var sb strings.Builder + for j < n && line[j] != '"' { + if line[j] == '\\' && j+1 < n { + j++ + } + sb.WriteByte(line[j]) + j++ + } + tokens = append(tokens, sb.String()) + i = j + 1 + + case '(': + depth := 1 + j := i + 1 + for j < n && depth > 0 { + switch line[j] { + case '(': + depth++ + case ')': + depth-- + } + j++ + } + tokens = append(tokens, line[i:j]) + i = j + + default: + j := i + for j < n && line[j] != ' ' && line[j] != '\t' { + j++ + } + tokens = append(tokens, line[i:j]) + i = j + } + } + return tokens +} + +// splitList takes a token like "(FLAGS UID)" and returns its inner +// space-separated items — used by FETCH/STORE argument parsing. +func splitList(token string) []string { + inner := strings.TrimPrefix(token, "(") + inner = strings.TrimSuffix(inner, ")") + if inner == "" { + return nil + } + return tokenize(inner) +} + +func isList(token string) bool { + return strings.HasPrefix(token, "(") && strings.HasSuffix(token, ")") +} diff --git a/internal/imap/server.go b/internal/imap/server.go new file mode 100644 index 0000000..fbf0b4b --- /dev/null +++ b/internal/imap/server.go @@ -0,0 +1,150 @@ +// Package imap implements a hand-rolled IMAP server covering the core +// command set (RFC 3501/9051 essentials): CAPABILITY, LOGIN, LOGOUT, NOOP, +// SELECT/EXAMINE, LIST, FETCH, UID FETCH, STORE, UID STORE, SEARCH, EXPUNGE, +// CLOSE, UNSELECT. No third-party IMAP library — stdlib net.Listener plus a +// small hand-written parser for IMAP's atom/quoted-string/literal syntax. +// +// Deferred to a later pass (noted here so the gap is visible, not hidden): +// IDLE, CONDSTORE/QRESYNC, SORT/THREAD, and mailbox CREATE/DELETE/RENAME. +// The core set above is enough for read/flag/delete workflows against an +// existing mailbox, which covers most mail client usage; IDLE (push) and +// folder management are the natural next additions. +package imap + +import ( + "context" + "crypto/tls" + "fmt" + "log/slog" + "net" + "sync" + "time" + + "gomail/internal/db" + "gomail/internal/mailstore" + "gomail/internal/ratelimit" +) + +const ( + idleTimeout = 30 * time.Minute // IMAP clients often sit connected much longer than SMTP + maxCommandLine = 8192 +) + +type Server struct { + database *db.DB + store *mailstore.Store + tlsConf *tls.Config + hostname string + + listeners []net.Listener + wg sync.WaitGroup + sessionWG sync.WaitGroup + + connLimiter *ratelimit.Limiter // per-IP connections/min + authLimiter *ratelimit.Limiter // per-IP LOGIN failures/min — checked before credential verification +} + +func NewServer(database *db.DB, store *mailstore.Store, tlsConf *tls.Config, hostname string, connPerMin, authFailuresPerMin int) *Server { + return &Server{ + database: database, + store: store, + tlsConf: tlsConf, + hostname: hostname, + connLimiter: ratelimit.New(connPerMin), + authLimiter: ratelimit.New(authFailuresPerMin), + } +} + +// ListenAndServe starts the plain (:143, STARTTLS-capable) and implicit-TLS +// (:993) listeners and blocks until ctx is cancelled or a listener fails. +func (s *Server) ListenAndServe(ctx context.Context, plainAddr, tlsAddr string) error { + specs := []struct { + addr string + useTLS bool + }{ + {plainAddr, false}, + {tlsAddr, true}, + } + + for _, spec := range specs { + ln, err := net.Listen("tcp", spec.addr) + if err != nil { + s.closeAll() + return fmt.Errorf("listen %s: %w", spec.addr, err) + } + if spec.useTLS { + ln = tls.NewListener(ln, s.tlsConf) + } + s.listeners = append(s.listeners, ln) + slog.Info("IMAP listener started", "addr", spec.addr, "implicit_tls", spec.useTLS) + + s.wg.Add(1) + go func(ln net.Listener) { + defer s.wg.Done() + s.acceptLoop(ctx, ln) + }(ln) + } + + <-ctx.Done() + return ctx.Err() +} + +func (s *Server) acceptLoop(ctx context.Context, ln net.Listener) { + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-ctx.Done(): + return + default: + slog.Error("IMAP accept error", "err", err) + return + } + } + ip := connHost(conn.RemoteAddr()) + if !s.connLimiter.Allow(ip) { + slog.Warn("IMAP connection rate limit exceeded, rejecting", "ip", ip) + conn.Close() + continue + } + + s.sessionWG.Add(1) + go func() { + defer s.sessionWG.Done() + sess := newSession(conn, s) + sess.run(ctx) + }() + } +} + +func (s *Server) Shutdown(gracePeriod time.Duration) { + s.closeAll() + done := make(chan struct{}) + go func() { + s.sessionWG.Wait() + close(done) + }() + select { + case <-done: + slog.Info("all IMAP sessions drained cleanly") + case <-time.After(gracePeriod): + slog.Warn("IMAP shutdown grace period expired — some sessions forcibly terminated") + } +} + +func (s *Server) closeAll() { + for _, ln := range s.listeners { + ln.Close() + } + s.wg.Wait() +} + +// connHost extracts just the IP (no port) from a net.Addr, for use as a +// rate-limiter key. +func connHost(addr net.Addr) string { + host, _, err := net.SplitHostPort(addr.String()) + if err != nil { + return addr.String() + } + return host +} diff --git a/internal/imap/session.go b/internal/imap/session.go new file mode 100644 index 0000000..4674e84 --- /dev/null +++ b/internal/imap/session.go @@ -0,0 +1,205 @@ +package imap + +import ( + "bufio" + "context" + "crypto/tls" + "fmt" + "io" + "log/slog" + "net" + "strings" + "time" + + "gomail/internal/auth" + "gomail/internal/db" +) + +type state int + +const ( + stateNotAuthenticated state = iota + stateAuthenticated + stateSelected +) + +type session struct { + conn net.Conn + rw *bufio.ReadWriter + server *Server + + state state + user *db.User + mailbox string + readOnly bool + tlsActive bool + + // snapshot of the selected mailbox's contents at SELECT time — IMAP + // sequence numbers are defined against this snapshot, not a live query, + // per standard IMAP semantics (changes appear as untagged responses on + // the next command in a fuller implementation; this pass re-snapshots on + // every SELECT/EXAMINE, which is correct as long as the client + // re-selects to see new mail — IDLE for live push is a later addition). + entries []db.MailboxEntry +} + +func newSession(conn net.Conn, server *Server) *session { + _, isTLS := conn.(*tls.Conn) + return &session{ + conn: conn, + rw: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + server: server, + state: stateNotAuthenticated, + tlsActive: isTLS, + } +} + +func (s *session) run(ctx context.Context) { + s.untagged(fmt.Sprintf("OK %s GoMail IMAP4rev1 ready", s.server.hostname)) + + for { + select { + case <-ctx.Done(): + s.untagged("BYE server shutting down") + return + default: + } + + s.conn.SetReadDeadline(time.Now().Add(idleTimeout)) + tag, cmd, args, err := s.readCommand() + if err != nil { + if err != io.EOF { + slog.Debug("IMAP read error", "err", err) + } + return + } + if !s.dispatch(tag, cmd, args) { + return + } + } +} + +// readCommand reads one line and tokenizes it into (tag, command, args). +// Literal syntax is intentionally unsupported in this pass (see parser.go +// doc comment) — a line ending in {n} is treated as a parse error rather +// than silently mishandled, so a client relying on literals gets a clear +// BAD response instead of the server hanging waiting for bytes that were +// never announced as expected. +func (s *session) readCommand() (tag, cmd string, args []string, err error) { + line, err := s.rw.ReadString('\n') + if err != nil { + return "", "", nil, err + } + line = strings.TrimRight(line, "\r\n") + if len(line) > maxCommandLine { + return "", "", nil, fmt.Errorf("command line too long") + } + + tokens := tokenize(line) + if len(tokens) < 2 { + return "", "", nil, fmt.Errorf("malformed command line: %q", line) + } + return tokens[0], strings.ToUpper(tokens[1]), tokens[2:], nil +} + +// dispatch runs one command. Returns false if the session should close. +func (s *session) dispatch(tag, cmd string, args []string) bool { + switch cmd { + case "CAPABILITY": + s.cmdCapability(tag) + case "STARTTLS": + s.cmdStartTLS(tag) + case "NOOP": + s.tagged(tag, "OK NOOP completed") + case "LOGOUT": + s.untagged("BYE GoMail IMAP4rev1 server logging out") + s.tagged(tag, "OK LOGOUT completed") + return false + case "LOGIN": + s.cmdLogin(tag, args) + case "AUTHENTICATE": + s.tagged(tag, "NO AUTHENTICATE not supported, use LOGIN") + case "SELECT": + s.cmdSelectExamine(tag, args, true) + case "EXAMINE": + s.cmdSelectExamine(tag, args, false) + case "LIST": + s.cmdList(tag, args) + case "LSUB": + s.cmdList(tag, args) // no separate subscription tracking yet — LSUB mirrors LIST + case "CLOSE": + s.cmdClose(tag) + case "UNSELECT": + s.mailbox = "" + s.entries = nil + s.state = stateAuthenticated + s.tagged(tag, "OK UNSELECT completed") + case "FETCH": + s.cmdFetch(tag, args, false) + case "STORE": + s.cmdStore(tag, args, false) + case "SEARCH": + s.cmdSearch(tag, args, false) + case "EXPUNGE": + s.cmdExpunge(tag) + case "UID": + s.cmdUID(tag, args) + default: + s.tagged(tag, "BAD command not recognized") + } + return true +} + +func (s *session) requireAuthenticated(tag string) bool { + if s.state == stateNotAuthenticated { + s.tagged(tag, "NO command requires authentication") + return false + } + return true +} + +func (s *session) requireSelected(tag string) bool { + if s.state != stateSelected { + s.tagged(tag, "NO command requires a selected mailbox") + return false + } + return true +} + +// ── I/O helpers ───────────────────────────────────────────────────────────────── + +func (s *session) tagged(tag, response string) { + s.rw.WriteString(tag + " " + response + "\r\n") + s.rw.Flush() +} + +func (s *session) untagged(response string) { + s.rw.WriteString("* " + response + "\r\n") + s.rw.Flush() +} + +func (s *session) continuation(text string) { + s.rw.WriteString("+ " + text + "\r\n") + s.rw.Flush() +} + +func (s *session) upgradeTLS(tlsConf *tls.Config) error { + tlsConn := tls.Server(s.conn, tlsConf) + if err := tlsConn.HandshakeContext(context.Background()); err != nil { + return err + } + s.conn = tlsConn + s.rw = bufio.NewReadWriter(bufio.NewReader(tlsConn), bufio.NewWriter(tlsConn)) + return nil +} + +// authenticateUser is the shared entry point LOGIN uses. +func (s *session) authenticateUser(username, password string) bool { + user, ok := auth.Authenticate(s.server.database, username, password, auth.ScopeIMAP) + if !ok { + return false + } + s.user = user + s.state = stateAuthenticated + return true +} diff --git a/internal/imap/tokenize_fuzz_test.go b/internal/imap/tokenize_fuzz_test.go new file mode 100644 index 0000000..71a49e7 --- /dev/null +++ b/internal/imap/tokenize_fuzz_test.go @@ -0,0 +1,33 @@ +package imap + +import "testing" + +func FuzzTokenize(f *testing.F) { + f.Add(`a001 LOGIN user pass`) + f.Add(`a002 SELECT INBOX`) + f.Add(`a003 FETCH 1:* (FLAGS UID)`) + f.Add(`a004 SEARCH UNSEEN`) + f.Add(`a005 STORE 1 +FLAGS (\Seen)`) + f.Add(`a006 LOGIN "quoted user" "quoted pass"`) + f.Add("") + f.Add(`(((((`) + f.Add(`"unterminated`) + f.Add(`a007 LIST "" *`) + f.Add(`a008 UID FETCH 1 (BODY[HEADER])`) + f.Add(`nested (parens (inside (parens)))`) + f.Add("\x00\x01\x02 binary garbage") + f.Add(`"escaped \" quote"`) + + f.Fuzz(func(t *testing.T, data string) { + // tokenize runs on every line a connected IMAP client sends, before + // any authentication has necessarily succeeded (e.g. the initial + // CAPABILITY/LOGIN exchange) — so it's exposed to fully untrusted + // network input and must never panic regardless of what's sent. + defer func() { + if r := recover(); r != nil { + t.Fatalf("tokenize panicked on input %q: %v", data, r) + } + }() + tokenize(data) + }) +} diff --git a/internal/imapclient/client.go b/internal/imapclient/client.go new file mode 100644 index 0000000..6aaae77 --- /dev/null +++ b/internal/imapclient/client.go @@ -0,0 +1,293 @@ +// Package imapclient is a minimal hand-rolled IMAP client used by +// provider_imap.go to talk to external IMAP servers (and, in tests, to +// GoMail's own IMAP server — proving client and server interoperate). No +// third-party IMAP library, matching the project's stdlib-first principle; +// this mirrors the parsing approach in internal/imap but for the client role. +package imapclient + +import ( + "bufio" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "net" + "regexp" + "strconv" + "strings" + "time" +) + +type Client struct { + conn net.Conn + r *bufio.Reader + w *bufio.Writer + tag int +} + +// Dial connects and reads the server greeting. useTLS=true dials directly +// into TLS (implicit-TLS port); otherwise the connection starts plaintext +// and the caller may call StartTLS. +func Dial(addr string, useTLS bool, tlsConf *tls.Config, timeout time.Duration) (*Client, error) { + conn, err := net.DialTimeout("tcp", addr, timeout) + if err != nil { + return nil, fmt.Errorf("dial %s: %w", addr, err) + } + conn.SetDeadline(time.Now().Add(timeout)) + + if useTLS { + conn = tls.Client(conn, tlsConf) + } + + c := &Client{conn: conn, r: bufio.NewReader(conn), w: bufio.NewWriter(conn)} + if _, err := c.readLine(); err != nil { // discard greeting text, just confirm we got one + return nil, fmt.Errorf("reading greeting: %w", err) + } + return c, nil +} + +func (c *Client) StartTLS(tlsConf *tls.Config) error { + if err := c.simpleCommand("STARTTLS"); err != nil { + return err + } + tlsConn := tls.Client(c.conn, tlsConf) + c.conn = tlsConn + c.r = bufio.NewReader(tlsConn) + c.w = bufio.NewWriter(tlsConn) + return nil +} + +func (c *Client) Login(username, password string) error { + return c.simpleCommand(fmt.Sprintf(`LOGIN %s %s`, quote(username), quote(password))) +} + +// LoginXOAUTH2 authenticates using an OAuth2 access token instead of a +// password — the mechanism Gmail and Microsoft 365 require for IMAP once +// "less secure app access" / basic auth is disabled, which is the default +// on both platforms today. saslPayload is base64-encoded here; callers +// build the raw payload via oauth2.XOAUTH2SASLString. +func (c *Client) LoginXOAUTH2(saslPayload string) error { + encoded := base64.StdEncoding.EncodeToString([]byte(saslPayload)) + _, tagged, err := c.command("AUTHENTICATE XOAUTH2 " + encoded) + if err != nil { + return err + } + if !strings.Contains(tagged, "OK") { + return fmt.Errorf("XOAUTH2 authentication failed: %s", tagged) + } + return nil +} + +func (c *Client) Logout() { + c.simpleCommand("LOGOUT") + c.conn.Close() +} + +// FolderInfo is a parsed LIST response entry. +type FolderInfo struct { + Name string +} + +func (c *Client) List() ([]FolderInfo, error) { + lines, tagged, err := c.command(`LIST "" "*"`) + if err != nil { + return nil, err + } + if !strings.Contains(tagged, "OK") { + return nil, fmt.Errorf("LIST failed: %s", tagged) + } + + var folders []FolderInfo + for _, line := range lines { + if !strings.Contains(line, "LIST") { + continue + } + // "* LIST () "/" INBOX" — take the last whitespace-separated token, + // stripping quotes if present. + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + name := strings.Trim(fields[len(fields)-1], `"`) + folders = append(folders, FolderInfo{Name: name}) + } + return folders, nil +} + +// SelectedInfo reports what a SELECT told us about the mailbox. +type SelectedInfo struct { + Exists int +} + +func (c *Client) Select(mailbox string) (*SelectedInfo, error) { + lines, tagged, err := c.command("SELECT " + quote(mailbox)) + if err != nil { + return nil, err + } + if !strings.Contains(tagged, "OK") { + return nil, fmt.Errorf("SELECT failed: %s", tagged) + } + + info := &SelectedInfo{} + existsRE := regexp.MustCompile(`^\* (\d+) EXISTS`) + for _, line := range lines { + if m := existsRE.FindStringSubmatch(line); m != nil { + info.Exists, _ = strconv.Atoi(m[1]) + } + } + return info, nil +} + +// FetchedMessage is one parsed FETCH response. +type FetchedMessage struct { + Seq int + UID int + Flags []string + Body []byte // present if BODY[] or BODY[HEADER] was requested +} + +// Fetch runs FETCH seqSet items and parses the responses. items should be +// the raw IMAP item list, e.g. "(UID FLAGS BODY[])". +func (c *Client) Fetch(seqSet, items string) ([]FetchedMessage, error) { + lines, tagged, err := c.command(fmt.Sprintf("FETCH %s %s", seqSet, items)) + if err != nil { + return nil, err + } + if !strings.Contains(tagged, "OK") { + return nil, fmt.Errorf("FETCH failed: %s", tagged) + } + return parseFetchLines(lines), nil +} + +// UIDFetch runs "UID FETCH " — the UID variant is a +// different command name on the wire (RFC 3501 §6.4.8), not a sequence-set +// prefix, so this is not just Fetch with a different first argument. +func (c *Client) UIDFetch(uidSet, items string) ([]FetchedMessage, error) { + lines, tagged, err := c.command(fmt.Sprintf("UID FETCH %s %s", uidSet, items)) + if err != nil { + return nil, err + } + if !strings.Contains(tagged, "OK") { + return nil, fmt.Errorf("UID FETCH failed: %s", tagged) + } + return parseFetchLines(lines), nil +} + +func (c *Client) Store(seqSet, action, flags string) error { + return c.simpleCommand(fmt.Sprintf("STORE %s %s (%s)", seqSet, action, flags)) +} + +// UIDStore is "UID STORE" — same command-name distinction as UIDFetch. +func (c *Client) UIDStore(uidSet, action, flags string) error { + return c.simpleCommand(fmt.Sprintf("UID STORE %s %s (%s)", uidSet, action, flags)) +} + +func (c *Client) Expunge() error { + return c.simpleCommand("EXPUNGE") +} + +// ── Command plumbing ──────────────────────────────────────────────────────────── + +// command sends one tagged command and returns every untagged response line +// plus the final tagged status line. +func (c *Client) command(cmd string) (untagged []string, tagged string, err error) { + c.tag++ + tag := fmt.Sprintf("C%03d", c.tag) + c.w.WriteString(tag + " " + cmd + "\r\n") + if err := c.w.Flush(); err != nil { + return nil, "", err + } + + for { + line, err := c.readLine() + if err != nil { + return nil, "", err + } + if strings.HasPrefix(line, tag+" ") { + return untagged, line, nil + } + untagged = append(untagged, line) + } +} + +func (c *Client) simpleCommand(cmd string) error { + _, tagged, err := c.command(cmd) + if err != nil { + return err + } + if !strings.Contains(tagged, "OK") { + return fmt.Errorf("%s failed: %s", strings.Fields(cmd)[0], tagged) + } + return nil +} + +var literalRE = regexp.MustCompile(`\{(\d+)\+?\}$`) + +// readLine reads one logical IMAP response line, transparently absorbing any +// literal ({N}\r\n) that appears in it — the literal's raw bytes +// (which may contain embedded CRLFs, exactly why literals exist) are spliced +// directly into the returned string, and reading continues until a line with +// no trailing literal marker is found, so a "BODY[] {123}\r\n<123 +// bytes>)\r\n" response comes back as one complete string ending in ")". +func (c *Client) readLine() (string, error) { + var full strings.Builder + for { + chunk, err := c.r.ReadString('\n') + if err != nil { + return "", err + } + chunk = strings.TrimRight(chunk, "\r\n") + full.WriteString(chunk) + + if m := literalRE.FindStringSubmatch(chunk); m != nil { + n, _ := strconv.Atoi(m[1]) + buf := make([]byte, n) + if _, err := io.ReadFull(c.r, buf); err != nil { + return "", fmt.Errorf("reading literal (%d bytes): %w", n, err) + } + full.WriteString(string(buf)) + continue // keep reading — more line content may follow the literal + } + return full.String(), nil + } +} + +// ── Parsing ─────────────────────────────────────────────────────────────────── + +var fetchHeaderRE = regexp.MustCompile(`(?s)^\* (\d+) FETCH \((.*)\)$`) +var uidRE = regexp.MustCompile(`UID (\d+)`) +var flagsRE = regexp.MustCompile(`FLAGS \(([^)]*)\)`) +var bodyRE = regexp.MustCompile(`(?s)BODY(?:\.PEEK)?\[[A-Z]*\] \{\d+\}(.*)$`) + +func parseFetchLines(lines []string) []FetchedMessage { + var out []FetchedMessage + for _, line := range lines { + m := fetchHeaderRE.FindStringSubmatch(line) + if m == nil { + continue + } + seq, _ := strconv.Atoi(m[1]) + rest := m[2] + + msg := FetchedMessage{Seq: seq} + if um := uidRE.FindStringSubmatch(rest); um != nil { + msg.UID, _ = strconv.Atoi(um[1]) + } + if fm := flagsRE.FindStringSubmatch(rest); fm != nil { + if fm[1] != "" { + msg.Flags = strings.Fields(fm[1]) + } + } + if bm := bodyRE.FindStringSubmatch(rest); bm != nil { + body := bm[1] + body = strings.TrimSuffix(body, ")") + msg.Body = []byte(body) + } + out = append(out, msg) + } + return out +} + +func quote(s string) string { + return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` +} diff --git a/internal/jmap/jmap.go b/internal/jmap/jmap.go new file mode 100644 index 0000000..25e74c0 --- /dev/null +++ b/internal/jmap/jmap.go @@ -0,0 +1,306 @@ +// Package jmap implements a subset of JMAP Core (RFC 8620) and JMAP Mail +// (RFC 8621) — enough for a real JMAP client to discover the session, +// list mailboxes, and query/fetch messages. Scoped deliberately: Email/set +// (flag changes, delete), Email/import (send), and push (EventSource) are +// deferred, along with Sieve/ManageSieve entirely (RFC 5804, not started +// this phase — noted here, not silently skipped, since ManageSieve was +// originally paired with this phase in the plan). +// +// This exists alongside — not instead of — Phase 8's direct REST API, +// which the webmail SPA still uses. JMAP here is independently testable +// and available for third-party JMAP clients per the plan's config toggle +// (jmap.external_enabled); a later pass can migrate the SPA's internals to +// call this instead without changing its own REST contract. +package jmap + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "gomail/internal/accounts" + "gomail/internal/auth" + "gomail/internal/db" + "gomail/internal/mailstore" +) + +const ( + coreCapability = "urn:ietf:params:jmap:core" + mailCapability = "urn:ietf:params:jmap:mail" +) + +type Handler struct { + database *db.DB + store *mailstore.Store + hostname string +} + +func NewHandler(database *db.DB, store *mailstore.Store, hostname string) *Handler { + return &Handler{database: database, store: store, hostname: hostname} +} + +func (h *Handler) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/.well-known/jmap", h.session) + mux.HandleFunc("/jmap/api", h.api) +} + +// ── Session resource (RFC 8620 §2) ───────────────────────────────────────────── + +func (h *Handler) session(w http.ResponseWriter, r *http.Request) { + user, ok := h.authenticate(r) + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="GoMail JMAP"`) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + resp := map[string]any{ + "capabilities": map[string]any{ + coreCapability: map[string]any{ + "maxSizeUpload": 50 * 1024 * 1024, + "maxConcurrentUpload": 4, + "maxSizeRequest": 10 * 1024 * 1024, + "maxConcurrentRequests": 4, + "maxCallsInRequest": 16, + "maxObjectsInGet": 500, + "maxObjectsInSet": 500, + }, + mailCapability: map[string]any{ + "maxMailboxesPerEmail": 10, + "maxMailboxDepth": 1, + "maxSizeMailboxName": 255, + "maxSizeAttachmentsPerEmail": 50 * 1024 * 1024, + "emailQuerySortOptions": []string{"receivedAt"}, + "mayCreateTopLevelMailbox": false, + }, + }, + "accounts": map[string]any{ + user.ID: map[string]any{ + "name": user.Email, + "isPersonal": true, + "isReadOnly": false, + "accountCapabilities": map[string]any{mailCapability: map[string]any{}}, + }, + }, + "primaryAccounts": map[string]string{mailCapability: user.ID}, + "username": user.Email, + "apiUrl": "/jmap/api", + "downloadUrl": "/jmap/download/{accountId}/{blobId}/{name}", + "uploadUrl": "/jmap/upload/{accountId}", + "eventSourceUrl": "/jmap/events", + "state": "1", + } + writeJSON(w, http.StatusOK, resp) +} + +func (h *Handler) authenticate(r *http.Request) (*db.User, bool) { + username, password, ok := r.BasicAuth() + if !ok { + return nil, false + } + return auth.Authenticate(h.database, username, password, auth.ScopeIMAP) +} + +// ── API endpoint (RFC 8620 §3) ────────────────────────────────────────────────── + +type request struct { + Using []string `json:"using"` + MethodCalls [][3]any `json:"methodCalls"` +} + +type response struct { + MethodResponses [][3]any `json:"methodResponses"` + SessionState string `json:"sessionState"` +} + +func (h *Handler) api(w http.ResponseWriter, r *http.Request) { + user, ok := h.authenticate(r) + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="GoMail JMAP"`) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid JMAP request: "+err.Error(), http.StatusBadRequest) + return + } + + resp := response{SessionState: "1"} + provider := accounts.NewGoMailProvider(h.database, h.store, user) + + for _, call := range req.MethodCalls { + methodName, _ := call[0].(string) + args, _ := call[1].(map[string]any) + callID, _ := call[2].(string) + + result := h.dispatch(r.Context(), provider, user, methodName, args) + resp.MethodResponses = append(resp.MethodResponses, [3]any{result.name, result.args, callID}) + } + + writeJSON(w, http.StatusOK, resp) +} + +type methodResult struct { + name string + args map[string]any +} + +func (h *Handler) dispatch(ctx context.Context, provider *accounts.GoMailProvider, user *db.User, method string, args map[string]any) methodResult { + switch method { + case "Core/echo": + return methodResult{name: "Core/echo", args: args} + + case "Mailbox/get": + return h.mailboxGet(ctx, provider, user) + + case "Email/query": + return h.emailQuery(ctx, provider, args) + + case "Email/get": + return h.emailGet(ctx, provider, args) + + default: + return methodResult{name: "error", args: map[string]any{"type": "unknownMethod", "description": fmt.Sprintf("method %q not implemented", method)}} + } +} + +// ── Mailbox/get ─────────────────────────────────────────────────────────────── + +func (h *Handler) mailboxGet(ctx context.Context, provider *accounts.GoMailProvider, user *db.User) methodResult { + folders, err := provider.ListFolders(ctx) + if err != nil { + return methodResult{name: "error", args: map[string]any{"type": "serverFail", "description": err.Error()}} + } + + var list []map[string]any + for _, f := range folders { + list = append(list, map[string]any{ + "id": f.ID, + "name": f.DisplayName, + "role": jmapRole(f.Type), + "totalEmails": f.TotalCount, + "unreadEmails": f.UnreadCount, + "parentId": nil, + "sortOrder": 0, + "isSubscribed": true, + }) + } + + return methodResult{name: "Mailbox/get", args: map[string]any{ + "accountId": user.ID, "state": "1", "list": list, "notFound": []string{}, + }} +} + +func jmapRole(folderType string) any { + switch folderType { + case "inbox": + return "inbox" + case "sent": + return "sent" + case "drafts": + return "drafts" + case "trash": + return "trash" + case "junk": + return "junk" + default: + return nil + } +} + +// ── Email/query ─────────────────────────────────────────────────────────────── + +func (h *Handler) emailQuery(ctx context.Context, provider *accounts.GoMailProvider, args map[string]any) methodResult { + filter, _ := args["filter"].(map[string]any) + mailboxID := "INBOX" + if filter != nil { + if m, ok := filter["inMailbox"].(string); ok && m != "" { + mailboxID = m + } + } + + headers, err := provider.ListMessages(ctx, mailboxID, accounts.ListOpts{}) + if err != nil { + return methodResult{name: "error", args: map[string]any{"type": "serverFail", "description": err.Error()}} + } + + ids := make([]string, len(headers)) + for i, hdr := range headers { + ids[i] = mailboxID + ":" + hdr.ID // composite ID since JMAP IDs are global, ours are per-folder + } + + return methodResult{name: "Email/query", args: map[string]any{ + "ids": ids, "queryState": "1", "canCalculateChanges": false, + "position": 0, "total": len(ids), + }} +} + +// ── Email/get ───────────────────────────────────────────────────────────────── + +func (h *Handler) emailGet(ctx context.Context, provider *accounts.GoMailProvider, args map[string]any) methodResult { + rawIDs, _ := args["ids"].([]any) + + var list []map[string]any + var notFound []string + for _, raw := range rawIDs { + compositeID, _ := raw.(string) + mailboxID, messageID, ok := splitCompositeID(compositeID) + if !ok { + notFound = append(notFound, compositeID) + continue + } + full, err := provider.GetMessage(ctx, mailboxID, messageID) + if err != nil { + notFound = append(notFound, compositeID) + continue + } + list = append(list, map[string]any{ + "id": compositeID, + "mailboxIds": map[string]bool{mailboxID: true}, + "from": []map[string]string{{"email": full.From}}, + "to": []map[string]string{{"email": full.To}}, + "subject": full.Subject, + "receivedAt": full.Date, + "size": full.SizeBytes, + "preview": truncatePreview(string(full.Raw)), + }) + } + + return methodResult{name: "Email/get", args: map[string]any{ + "state": "1", "list": list, "notFound": notFound, + }} +} + +func splitCompositeID(id string) (mailboxID, messageID string, ok bool) { + idx := strings.LastIndex(id, ":") + if idx == -1 { + return "", "", false + } + return id[:idx], id[idx+1:], true +} + +func truncatePreview(raw string) string { + sep := "\r\n\r\n" + body := raw + if idx := strings.Index(raw, sep); idx >= 0 { + body = raw[idx+len(sep):] + } + if len(body) > 200 { + body = body[:200] + } + return body +} + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(v) +} diff --git a/internal/mailstore/maildir.go b/internal/mailstore/maildir.go new file mode 100644 index 0000000..65eed8f --- /dev/null +++ b/internal/mailstore/maildir.go @@ -0,0 +1,242 @@ +// Package mailstore implements Maildir++-style on-disk message storage with +// every message encrypted at rest (AES-256-GCM, per-message key derived via +// HKDF from the master key — see internal/crypto). +package mailstore + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "gomail/internal/crypto" + "gomail/internal/db" + "github.com/google/uuid" +) + +// Store writes and reads encrypted messages in a Maildir++ layout: +// +// {root}/{user-email}/{mailbox}/cur/{filename}.eml.enc +// {root}/{user-email}/{mailbox}/new/ +// {root}/{user-email}/{mailbox}/tmp/ +type Store struct { + root string + mk *crypto.MasterKey + db *db.DB +} + +func New(root string, mk *crypto.MasterKey, database *db.DB) *Store { + return &Store{root: root, mk: mk, db: database} +} + +// Deliver writes a raw message into a user's mailbox, encrypting it at rest, +// allocates the next IMAP UID, and records the mailbox_index row. Returns the +// assigned UID. +func (s *Store) Deliver(userID, userEmail, mailbox string, raw []byte) (uid int, err error) { + if err := s.ensureMailboxDirs(userEmail, mailbox); err != nil { + return 0, err + } + + messageID := uuid.NewString() + encrypted, err := crypto.Encrypt(s.mk, messageID, "message", raw) + if err != nil { + return 0, fmt.Errorf("encrypt message: %w", err) + } + + filename := maildirFilename(messageID) + tmpPath := filepath.Join(s.mailboxDir(userEmail, mailbox), "tmp", filename) + finalPath := filepath.Join(s.mailboxDir(userEmail, mailbox), "cur", filename) + + // Write to tmp/ then atomically rename into cur/ — standard Maildir delivery + // guarantee: a reader never observes a partially-written file. + if err := os.WriteFile(tmpPath, encrypted, 0600); err != nil { + return 0, fmt.Errorf("write tmp file: %w", err) + } + if err := os.Rename(tmpPath, finalPath); err != nil { + os.Remove(tmpPath) + return 0, fmt.Errorf("atomic rename: %w", err) + } + + allocatedUID, err := s.db.NextMailboxUID(userID, mailbox) + if err != nil { + return 0, fmt.Errorf("allocate uid: %w", err) + } + + entry := &db.MailboxEntry{ + ID: messageID, + UserID: userID, + Mailbox: mailbox, + UID: allocatedUID, + EMLPath: finalPath, + Flags: "", + SizeBytes: int64(len(raw)), + ReceivedAt: time.Now().UTC(), + InternalDate: time.Now().UTC(), + } + if err := s.db.InsertMailboxEntry(entry); err != nil { + // Best-effort cleanup of the file we just wrote — DB is the source of + // truth for what "exists"; an orphaned encrypted file with no index + // row is inert and harmless, but we try to avoid leaving one anyway. + os.Remove(finalPath) + return 0, fmt.Errorf("index mailbox entry: %w", err) + } + + return allocatedUID, nil +} + +// Read decrypts and returns the raw message bytes for a given encrypted file path. +// messageID must match the ID used at Deliver time (it's embedded in the filename). +func (s *Store) Read(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read encrypted file: %w", err) + } + + messageID := messageIDFromFilename(filepath.Base(path)) + plaintext, err := crypto.Decrypt(s.mk, messageID, "message", data) + if err != nil { + return nil, fmt.Errorf("decrypt message: %w", err) + } + return plaintext, nil +} + +func (s *Store) ensureMailboxDirs(userEmail, mailbox string) error { + base := s.mailboxDir(userEmail, mailbox) + for _, sub := range []string{"cur", "new", "tmp"} { + if err := os.MkdirAll(filepath.Join(base, sub), 0700); err != nil { + return fmt.Errorf("creating maildir %s/%s: %w", mailbox, sub, err) + } + } + return nil +} + +func (s *Store) mailboxDir(userEmail, mailbox string) string { + safeUser := sanitizePathComponent(userEmail) + safeMailbox := sanitizePathComponent(mailbox) + return filepath.Join(s.root, safeUser, safeMailbox) +} + +// sanitizePathComponent prevents path traversal via crafted mailbox names or +// email addresses — strips any path separators or parent-directory references. +func sanitizePathComponent(s string) string { + s = strings.ReplaceAll(s, "/", "_") + s = strings.ReplaceAll(s, "\\", "_") + s = strings.ReplaceAll(s, "..", "_") + s = strings.TrimSpace(s) + if s == "" { + s = "_" + } + return s +} + +// maildirFilename builds a Maildir-spec-ish unique filename embedding the +// message ID (needed later to re-derive the decryption key) plus a random +// suffix for readability/uniqueness under concurrent delivery. +func maildirFilename(messageID string) string { + suffix := make([]byte, 4) + rand.Read(suffix) + return fmt.Sprintf("%d.%s.%s.eml.enc", time.Now().UnixNano(), messageID, hex.EncodeToString(suffix)) +} + +func messageIDFromFilename(filename string) string { + parts := strings.Split(filename, ".") + if len(parts) >= 2 { + return parts[1] + } + return "" +} + +// WriteQueueFile encrypts and stores a message destined for outbound +// delivery, separately from any user's Maildir (it's not "in" a mailbox +// until/unless it becomes a Sent-folder copy after successful delivery — +// that wiring lands with the webmail Sent view in a later phase). Returns +// the messageID (used as both the encryption record ID and to find the file +// again) and the file path to record in outbound_queue.eml_path. +func (s *Store) WriteQueueFile(raw []byte) (messageID, path string, err error) { + queueDir := filepath.Join(s.root, ".queue") + if err := os.MkdirAll(queueDir, 0700); err != nil { + return "", "", fmt.Errorf("creating queue dir: %w", err) + } + + messageID = uuid.NewString() + encrypted, err := crypto.Encrypt(s.mk, messageID, "message", raw) + if err != nil { + return "", "", fmt.Errorf("encrypt queued message: %w", err) + } + + filename := maildirFilename(messageID) + tmpPath := filepath.Join(queueDir, "tmp-"+filename) + finalPath := filepath.Join(queueDir, filename) + + if err := os.WriteFile(tmpPath, encrypted, 0600); err != nil { + return "", "", fmt.Errorf("write queue tmp file: %w", err) + } + if err := os.Rename(tmpPath, finalPath); err != nil { + os.Remove(tmpPath) + return "", "", fmt.Errorf("atomic rename: %w", err) + } + + return messageID, finalPath, nil +} + +// DeleteQueueFile removes a queue file after successful delivery or a +// generated bounce — called by the queue worker. +func (s *Store) DeleteQueueFile(path string) error { + return os.Remove(path) +} + +// WriteQuarantineFile encrypts and stores a held message using the given +// messageID (the same ID as its `messages` audit row) rather than generating +// a new one — so release-time decryption can key off the ID already on hand +// from the quarantine/messages tables without needing to parse it back out +// of a filename. +func (s *Store) WriteQuarantineFile(messageID string, raw []byte) (path string, err error) { + qDir := filepath.Join(s.root, ".quarantine") + if err := os.MkdirAll(qDir, 0700); err != nil { + return "", fmt.Errorf("creating quarantine dir: %w", err) + } + + encrypted, err := crypto.Encrypt(s.mk, messageID, "message", raw) + if err != nil { + return "", fmt.Errorf("encrypt quarantined message: %w", err) + } + + filename := messageID + ".eml.enc" + tmpPath := filepath.Join(qDir, "tmp-"+filename) + finalPath := filepath.Join(qDir, filename) + + if err := os.WriteFile(tmpPath, encrypted, 0600); err != nil { + return "", fmt.Errorf("write quarantine tmp file: %w", err) + } + if err := os.Rename(tmpPath, finalPath); err != nil { + os.Remove(tmpPath) + return "", fmt.Errorf("atomic rename: %w", err) + } + return finalPath, nil +} + +// ReadQuarantineFile decrypts a quarantined message given its messageID +// (needed because quarantine files are keyed by ID directly, not embedded +// in the filename the way Deliver's maildirFilename embeds it). +func (s *Store) ReadQuarantineFile(messageID, path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read quarantine file: %w", err) + } + return crypto.Decrypt(s.mk, messageID, "message", data) +} +// a large message is needed (future IMAP partial FETCH support) — for now it +// simply decrypts and returns the full body since GCM doesn't support +// streaming partial decryption without the full ciphertext. +func (s *Store) ReadAt(path string, w io.Writer) error { + data, err := s.Read(path) + if err != nil { + return err + } + _, err = w.Write(data) + return err +} diff --git a/internal/managesieve/server.go b/internal/managesieve/server.go new file mode 100644 index 0000000..6afbcff --- /dev/null +++ b/internal/managesieve/server.go @@ -0,0 +1,90 @@ +// Package managesieve implements a RFC 5804 ManageSieve server — the +// protocol mail clients (Thunderbird's Sieve plugin, etc.) use to upload and +// manage server-side filtering scripts. Every uploaded script is validated +// with internal/sieve's parser before being stored, so a syntactically +// invalid script is rejected at PUTSCRIPT time rather than silently failing +// at delivery time. +package managesieve + +import ( + "context" + "crypto/tls" + "fmt" + "log/slog" + "net" + "sync" + "time" + + "gomail/internal/db" +) + +const idleTimeout = 10 * time.Minute + +type Server struct { + database *db.DB + tlsConf *tls.Config + hostname string + + listener net.Listener + wg sync.WaitGroup + sessionWG sync.WaitGroup +} + +func NewServer(database *db.DB, tlsConf *tls.Config, hostname string) *Server { + return &Server{database: database, tlsConf: tlsConf, hostname: hostname} +} + +func (s *Server) ListenAndServe(ctx context.Context, addr string) error { + ln, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("listen %s: %w", addr, err) + } + s.listener = ln + slog.Info("ManageSieve listener started", "addr", addr) + + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.acceptLoop(ctx, ln) + }() + + <-ctx.Done() + return ctx.Err() +} + +func (s *Server) acceptLoop(ctx context.Context, ln net.Listener) { + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-ctx.Done(): + return + default: + slog.Error("ManageSieve accept error", "err", err) + return + } + } + s.sessionWG.Add(1) + go func() { + defer s.sessionWG.Done() + newSession(conn, s).run(ctx) + }() + } +} + +func (s *Server) Shutdown(gracePeriod time.Duration) { + if s.listener != nil { + s.listener.Close() + } + s.wg.Wait() + done := make(chan struct{}) + go func() { + s.sessionWG.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(gracePeriod): + slog.Warn("ManageSieve shutdown grace period expired") + } +} diff --git a/internal/managesieve/session.go b/internal/managesieve/session.go new file mode 100644 index 0000000..e313166 --- /dev/null +++ b/internal/managesieve/session.go @@ -0,0 +1,359 @@ +package managesieve + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "log/slog" + "net" + "strconv" + "strings" + "time" + + "gomail/internal/auth" + "gomail/internal/db" + "gomail/internal/sieve" + "github.com/google/uuid" +) + +type session struct { + conn net.Conn + rw *bufio.ReadWriter + server *Server + tlsActive bool + user *db.User +} + +func newSession(conn net.Conn, server *Server) *session { + _, isTLS := conn.(*tls.Conn) + return &session{ + conn: conn, + rw: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + server: server, + tlsActive: isTLS, + } +} + +func (s *session) run(ctx context.Context) { + s.sendCapabilities() + + for { + select { + case <-ctx.Done(): + s.writeLine(`BYE "server shutting down"`) + return + default: + } + + s.conn.SetReadDeadline(time.Now().Add(idleTimeout)) + line, err := s.readLine() + if err != nil { + if err != io.EOF { + slog.Debug("ManageSieve read error", "err", err) + } + return + } + if !s.dispatch(line) { + return + } + } +} + +func (s *session) sendCapabilities() { + s.writeLine(`"IMPLEMENTATION" "GoMail ManageSieve"`) + s.writeLine(`"SIEVE" "fileinto"`) + s.writeLine(`"VERSION" "1.0"`) + if !s.tlsActive { + s.writeLine(`"STARTTLS"`) + } + s.writeLine("OK") +} + +func (s *session) dispatch(line string) bool { + verb, rest := splitVerb(line) + switch strings.ToUpper(verb) { + case "CAPABILITY": + s.sendCapabilities() + case "STARTTLS": + s.cmdStartTLS() + case "AUTHENTICATE": + s.cmdAuthenticate(rest) + case "LOGOUT": + s.writeLine("OK") + return false + case "PUTSCRIPT": + s.cmdPutScript(rest) + case "GETSCRIPT": + s.cmdGetScript(rest) + case "LISTSCRIPTS": + s.cmdListScripts() + case "SETACTIVE": + s.cmdSetActive(rest) + case "DELETESCRIPT": + s.cmdDeleteScript(rest) + case "NOOP": + s.writeLine("OK") + default: + s.writeLine(`NO "command not recognized"`) + } + return true +} + +func (s *session) cmdStartTLS() { + if s.tlsActive { + s.writeLine(`NO "TLS already active"`) + return + } + s.writeLine("OK") + tlsConn := tls.Server(s.conn, s.server.tlsConf) + if err := tlsConn.HandshakeContext(context.Background()); err != nil { + return + } + s.conn = tlsConn + s.rw = bufio.NewReadWriter(bufio.NewReader(tlsConn), bufio.NewWriter(tlsConn)) + s.tlsActive = true +} + +// cmdAuthenticate handles AUTHENTICATE "PLAIN" — the SASL PLAIN +// mechanism, same as SMTP/IMAP's AUTH PLAIN, adapted to ManageSieve's quoted +// string argument syntax rather than a bare base64 token. +func (s *session) cmdAuthenticate(rest string) { + if !s.tlsActive { + s.writeLine(`NO "authentication requires TLS — use STARTTLS first"`) + return + } + + parts := splitQuotedArgs(rest) + if len(parts) < 1 || strings.ToUpper(strings.Trim(parts[0], `"`)) != "PLAIN" { + s.writeLine(`NO "only AUTHENTICATE PLAIN is supported"`) + return + } + + var b64 string + if len(parts) >= 2 { + b64 = strings.Trim(parts[1], `"`) + } else { + s.writeLine("{0}") + line, err := s.readLine() + if err != nil { + return + } + b64 = line + } + + decoded, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + s.writeLine(`NO "malformed SASL response"`) + return + } + fields := strings.SplitN(string(decoded), "\x00", 3) + if len(fields) != 3 { + s.writeLine(`NO "malformed SASL PLAIN payload"`) + return + } + username, password := fields[1], fields[2] + + user, ok := auth.Authenticate(s.server.database, username, password, auth.ScopeIMAP) + if !ok { + s.writeLine(`NO "authentication failed"`) + return + } + s.user = user + s.writeLine("OK") +} + +func (s *session) requireAuth() bool { + if s.user == nil { + s.writeLine(`NO "authentication required"`) + return false + } + return true +} + +// cmdPutScript handles: PUTSCRIPT "name" {N+}\r\n\r\n +func (s *session) cmdPutScript(rest string) { + if !s.requireAuth() { + return + } + parts := splitQuotedArgs(rest) + if len(parts) < 1 { + s.writeLine(`NO "PUTSCRIPT requires a script name"`) + return + } + name := strings.Trim(parts[0], `"`) + + scriptText, err := s.readLiteralFromRemainder(rest) + if err != nil { + s.writeLine(`NO "expected script literal: ` + err.Error() + `"`) + return + } + + if _, err := sieve.Parse(scriptText); err != nil { + s.writeLine(`NO "script failed to parse: ` + escapeQuoted(err.Error()) + `"`) + return + } + + if err := s.server.database.UpsertSieveScript(&db.SieveScript{ + ID: uuid.NewString(), UserID: s.user.ID, Name: name, ScriptText: scriptText, + }); err != nil { + s.writeLine(`NO "storage error"`) + return + } + s.writeLine("OK") +} + +func (s *session) cmdGetScript(rest string) { + if !s.requireAuth() { + return + } + name := strings.Trim(strings.TrimSpace(rest), `"`) + script, err := s.server.database.GetSieveScript(s.user.ID, name) + if err != nil { + s.writeLine(`NO "script not found"`) + return + } + s.writeLine(fmt.Sprintf("{%d}", len(script.ScriptText))) + s.rw.WriteString(script.ScriptText) + s.rw.WriteString("\r\n") + s.rw.Flush() + s.writeLine("OK") +} + +func (s *session) cmdListScripts() { + if !s.requireAuth() { + return + } + scripts, err := s.server.database.ListSieveScripts(s.user.ID) + if err != nil { + s.writeLine(`NO "storage error"`) + return + } + for _, sc := range scripts { + if sc.Active { + s.writeLine(fmt.Sprintf(`"%s" ACTIVE`, sc.Name)) + } else { + s.writeLine(fmt.Sprintf(`"%s"`, sc.Name)) + } + } + s.writeLine("OK") +} + +func (s *session) cmdSetActive(rest string) { + if !s.requireAuth() { + return + } + name := strings.Trim(strings.TrimSpace(rest), `"`) + if name == "" { + // Empty name deactivates all scripts, per RFC 5804 §2.9. + s.server.database.Exec(`UPDATE sieve_scripts SET active = 0 WHERE user_id = ?`, s.user.ID) + s.writeLine("OK") + return + } + if err := s.server.database.SetActiveSieveScript(s.user.ID, name); err != nil { + s.writeLine(`NO "script not found"`) + return + } + s.writeLine("OK") +} + +func (s *session) cmdDeleteScript(rest string) { + if !s.requireAuth() { + return + } + name := strings.Trim(strings.TrimSpace(rest), `"`) + if err := s.server.database.DeleteSieveScript(s.user.ID, name); err != nil { + s.writeLine(`NO "delete failed"`) + return + } + s.writeLine("OK") +} + +// ── I/O helpers ───────────────────────────────────────────────────────────────── + +func (s *session) writeLine(line string) { + s.rw.WriteString(line + "\r\n") + s.rw.Flush() +} + +func (s *session) readLine() (string, error) { + line, err := s.rw.ReadString('\n') + if err != nil { + return "", err + } + return strings.TrimRight(line, "\r\n"), nil +} + +// readLiteralFromRemainder expects the command line's remainder to end in a +// {N} or {N+} literal announcement (RFC 5804 reuses IMAP-style literal +// syntax) and reads exactly N raw bytes following it. +func (s *session) readLiteralFromRemainder(rest string) (string, error) { + idx := strings.LastIndex(rest, "{") + if idx == -1 || !strings.HasSuffix(strings.TrimSpace(rest), "}") { + return "", fmt.Errorf("no literal size announced") + } + sizeStr := strings.TrimSuffix(strings.TrimSpace(rest[idx+1:]), "}") + sizeStr = strings.TrimSuffix(sizeStr, "+") + n, err := strconv.Atoi(sizeStr) + if err != nil { + return "", fmt.Errorf("invalid literal size: %w", err) + } + + buf := make([]byte, n) + if _, err := io.ReadFull(s.rw, buf); err != nil { + return "", fmt.Errorf("reading literal: %w", err) + } + s.rw.ReadString('\n') // consume trailing CRLF after the literal bytes + return string(buf), nil +} + +func splitVerb(line string) (verb, rest string) { + line = strings.TrimSpace(line) + i := strings.IndexAny(line, " \t") + if i < 0 { + return line, "" + } + return line[:i], strings.TrimSpace(line[i+1:]) +} + +// splitQuotedArgs splits `"arg1" "arg2"` into ["arg1","arg2"] (quotes kept, +// stripped by callers as needed) — tolerant of a trailing {N+} literal +// marker, which callers handle separately via readLiteralFromRemainder. +func splitQuotedArgs(s string) []string { + var args []string + i := 0 + for i < len(s) { + for i < len(s) && s[i] == ' ' { + i++ + } + if i >= len(s) { + break + } + if s[i] == '"' { + j := i + 1 + for j < len(s) && s[j] != '"' { + j++ + } + if j < len(s) { + args = append(args, s[i:j+1]) + i = j + 1 + } else { + break + } + } else { + j := i + for j < len(s) && s[j] != ' ' { + j++ + } + args = append(args, s[i:j]) + i = j + } + } + return args +} + +func escapeQuoted(s string) string { + return strings.ReplaceAll(s, `"`, `'`) +} diff --git a/internal/oauth2/oauth2.go b/internal/oauth2/oauth2.go new file mode 100644 index 0000000..a47508f --- /dev/null +++ b/internal/oauth2/oauth2.go @@ -0,0 +1,163 @@ +// Package oauth2 implements the OAuth2 authorization code grant (RFC 6749 +// §4.1) and token refresh (§6) — hand-rolled on net/http + encoding/json, +// no third-party OAuth2 library, matching the project's dependency-minimal +// principle. This is genuinely small (~150 lines) once you're not carrying +// a general-purpose library's support for every grant type GoMail doesn't +// use. +package oauth2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// Config holds one provider's OAuth2 app registration — operator-supplied +// (Client ID/Secret from their own Google Cloud / Azure AD app +// registration, per the plan's "self-hosted operators register their own +// app" decision) plus the provider's well-known endpoints. +type Config struct { + ClientID string + ClientSecret string + RedirectURI string + AuthURL string + TokenURL string + Scopes []string +} + +// WellKnownEndpoints returns the real, fixed endpoint URLs for supported +// providers — these are NOT operator-configurable (only ClientID/Secret +// are), since pointing "google" at an arbitrary URL would defeat the point +// of naming a known provider. Tests construct a Config directly with +// endpoints pointed at a local fake server instead of using this function. +func WellKnownEndpoints(provider string) (authURL, tokenURL string, err error) { + switch provider { + case "google": + return "https://accounts.google.com/o/oauth2/v2/auth", "https://oauth2.googleapis.com/token", nil + case "microsoft": + return "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + "https://login.microsoftonline.com/common/oauth2/v2.0/token", nil + default: + return "", "", fmt.Errorf("unknown provider %q", provider) + } +} + +// Token is what the provider returns from a code exchange or refresh. +type Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` // may be empty on a refresh response — providers don't always rotate it + TokenType string `json:"token_type"` + ExpiresAt time.Time `json:"expires_at"` +} + +type tokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Error string `json:"error"` + ErrorDesc string `json:"error_description"` +} + +// BuildAuthURL constructs the URL to redirect the user's browser to. state +// is a caller-generated random value (CSRF protection — the caller must +// verify the same value comes back on the callback) — see webmail's +// oauthStart handler for how it's generated and stored. +func (c *Config) BuildAuthURL(state string) string { + v := url.Values{} + v.Set("client_id", c.ClientID) + v.Set("redirect_uri", c.RedirectURI) + v.Set("response_type", "code") + v.Set("scope", strings.Join(c.Scopes, " ")) + v.Set("state", state) + v.Set("access_type", "offline") // request a refresh_token (Google-specific but harmless elsewhere) + v.Set("prompt", "consent") + return c.AuthURL + "?" + v.Encode() +} + +// ExchangeCode trades an authorization code (from the callback's ?code= +// query param) for an access + refresh token. +func (c *Config) ExchangeCode(ctx context.Context, code string) (*Token, error) { + form := url.Values{} + form.Set("client_id", c.ClientID) + form.Set("client_secret", c.ClientSecret) + form.Set("redirect_uri", c.RedirectURI) + form.Set("code", code) + form.Set("grant_type", "authorization_code") + return c.doTokenRequest(ctx, form) +} + +// RefreshToken exchanges a stored refresh token for a new access token. +func (c *Config) RefreshToken(ctx context.Context, refreshToken string) (*Token, error) { + form := url.Values{} + form.Set("client_id", c.ClientID) + form.Set("client_secret", c.ClientSecret) + form.Set("refresh_token", refreshToken) + form.Set("grant_type", "refresh_token") + tok, err := c.doTokenRequest(ctx, form) + if err != nil { + return nil, err + } + if tok.RefreshToken == "" { + tok.RefreshToken = refreshToken // providers often omit it on refresh — keep the old one + } + return tok, nil +} + +func (c *Config) doTokenRequest(ctx context.Context, form url.Values) (*Token, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.TokenURL, strings.NewReader(form.Encode())) + if err != nil { + return nil, fmt.Errorf("building token request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("token request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading token response: %w", err) + } + + var tr tokenResponse + if err := json.Unmarshal(body, &tr); err != nil { + return nil, fmt.Errorf("parsing token response: %w (body: %s)", err, truncate(body, 200)) + } + if tr.Error != "" { + return nil, fmt.Errorf("oauth2 error: %s (%s)", tr.Error, tr.ErrorDesc) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("token endpoint returned status %d: %s", resp.StatusCode, truncate(body, 200)) + } + + return &Token{ + AccessToken: tr.AccessToken, + RefreshToken: tr.RefreshToken, + TokenType: tr.TokenType, + ExpiresAt: time.Now().UTC().Add(time.Duration(tr.ExpiresIn) * time.Second), + }, nil +} + +func truncate(b []byte, n int) string { + if len(b) > n { + return string(b[:n]) + "..." + } + return string(b) +} + +// XOAUTH2SASLString builds the SASL XOAUTH2 initial-response string (used +// by IMAP/SMTP clients authenticating with an OAuth2 access token instead +// of a password) per Google's documented format, which Microsoft also +// accepts for IMAP: "user=\x01auth=Bearer \x01\x01". +func XOAUTH2SASLString(email, accessToken string) string { + return "user=" + email + "\x01auth=Bearer " + accessToken + "\x01\x01" +} diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go new file mode 100644 index 0000000..4159357 --- /dev/null +++ b/internal/pipeline/pipeline.go @@ -0,0 +1,163 @@ +// Package pipeline implements the inbound security pipeline: SPF, DKIM +// verification, DMARC, header/URL heuristics — each stage contributes a +// score, and the orchestrator maps the total score to a verdict (clean, +// flagged, quarantine, blocked) per the configured thresholds. +// +// Every stage is blocking and runs before the SMTP DATA response — no +// third-party spam-filtering library, entirely stdlib DNS/crypto/net/mail. +package pipeline + +import ( + "context" + "net" + "net/mail" + "strings" + "time" + + "gomail/internal/config" + "gomail/internal/db" +) + +// MailContext carries everything a stage needs and accumulates results. +type MailContext struct { + SenderIP net.IP + SenderHost string + MailFrom string + RcptTo string + RawMessage []byte + + Checks []StageResult + TotalScore float64 + Verdict db.MessageVerdict + + parsedMessage *mail.Message + parseErr error + parseAttempted bool +} + +// ParsedMessage lazily parses RawMessage via net/mail — stages call this +// instead of parsing independently, so the (relatively expensive) header +// parse happens at most once per message regardless of how many stages need it. +func (mc *MailContext) ParsedMessage() (*mail.Message, error) { + if !mc.parseAttempted { + mc.parsedMessage, mc.parseErr = mail.ReadMessage(strings.NewReader(string(mc.RawMessage))) + mc.parseAttempted = true + } + return mc.parsedMessage, mc.parseErr +} + +// RcptDomain returns the domain portion of RcptTo. +func (mc *MailContext) RcptDomain() string { + return domainOf(mc.RcptTo) +} + +// MailFromDomain returns the domain portion of the envelope sender. +func (mc *MailContext) MailFromDomain() string { + return domainOf(mc.MailFrom) +} + +func domainOf(addr string) string { + parts := strings.SplitN(strings.ToLower(addr), "@", 2) + if len(parts) == 2 { + return parts[1] + } + return "" +} + +// StageResult is one check's outcome. +type StageResult struct { + Stage string + Result db.CheckResult + Score float64 + Detail string + DurationMs int64 +} + +// Stage is one pipeline check. Run must not block indefinitely — pass a +// context with a deadline and respect it for any network I/O (DNS lookups). +type Stage interface { + Name() string + Run(ctx context.Context, mc *MailContext) *StageResult +} + +// Orchestrator runs the configured stages in order and computes the verdict. +type Orchestrator struct { + stages []Stage + cfg *config.Config +} + +func NewOrchestrator(cfg *config.Config, stages []Stage) *Orchestrator { + return &Orchestrator{stages: stages, cfg: cfg} +} + +// DefaultStages returns the deterministic, always-on stage set (SPF, DKIM, +// DMARC, header anomaly, URL heuristics) — no external service +// dependencies, always safe to run regardless of what's configured. +func DefaultStages() []Stage { + return []Stage{ + &SPFStage{}, + &DKIMStage{}, + &DMARCStage{}, + &HeaderStage{}, + &URLStage{}, + } +} + +// StagesFromConfig returns DefaultStages() plus any of the optional +// external-service stages (ClamAV, Rspamd, LLM) that config has an address +// configured for — each is entirely absent from the pipeline, not merely +// disabled, when its config field is empty, so an unreachable/misconfigured +// service that was never intended to be used can't accidentally affect +// delivery. +func StagesFromConfig(cfg *config.Config) []Stage { + stages := DefaultStages() + + if cfg.Pipeline.ClamAVSocket != "" { + stages = append(stages, &ClamAVStage{Addr: cfg.Pipeline.ClamAVSocket, Timeout: 30 * time.Second}) + } + if cfg.Pipeline.RspamdURL != "" { + stages = append(stages, &RspamdStage{BaseURL: cfg.Pipeline.RspamdURL, Timeout: 15 * time.Second}) + } + if cfg.Pipeline.LLMURL != "" { + timeout := time.Duration(cfg.Pipeline.LLMTimeoutSecs) * time.Second + if timeout <= 0 { + timeout = 30 * time.Second + } + stages = append(stages, &LLMStage{BaseURL: cfg.Pipeline.LLMURL, Model: cfg.Pipeline.LLMModel, Timeout: timeout}) + } + + return stages +} + +// Run executes every stage in order, accumulating score, and computes the +// final verdict against the configured thresholds. Individual stage panics +// are not recovered here deliberately — a panicking stage is a bug that +// should surface loudly in testing, not be silently swallowed in production +// and misclassify mail. +func (o *Orchestrator) Run(ctx context.Context, mc *MailContext) { + for _, stage := range o.stages { + start := time.Now() + result := stage.Run(ctx, mc) + if result == nil { + continue + } + result.DurationMs = time.Since(start).Milliseconds() + mc.Checks = append(mc.Checks, *result) + mc.TotalScore += result.Score + } + + mc.Verdict = verdictFor(mc.TotalScore, o.cfg.Pipeline) +} + +func verdictFor(score float64, p config.PipelineConfig) db.MessageVerdict { + switch { + case score >= p.ScoreBlock: + return db.VerdictBlocked + case score >= p.ScoreQuarantine: + return db.VerdictQuarantine + case score >= p.ScoreFlag: + return db.VerdictFlagged + default: + return db.VerdictClean + } +} diff --git a/internal/pipeline/stage_clamav.go b/internal/pipeline/stage_clamav.go new file mode 100644 index 0000000..10f2b23 --- /dev/null +++ b/internal/pipeline/stage_clamav.go @@ -0,0 +1,127 @@ +package pipeline + +import ( + "context" + "encoding/binary" + "fmt" + "net" + "strings" + "time" + + "gomail/internal/db" +) + +// ClamAVStage scans the raw message via clamd's INSTREAM protocol — a +// small, well-documented binary protocol (no third-party clamd client +// library): send "zINSTREAM\0", then the message in 4-byte-big-endian- +// length-prefixed chunks terminated by a zero-length chunk, then read the +// single-line response ("stream: OK", "stream: FOUND", or +// "stream: "). Off by default — only active when +// config.Pipeline.ClamAVSocket is set. +type ClamAVStage struct { + Addr string // "unix:/var/run/clamav/clamd.ctl" or "tcp:127.0.0.1:3310" + Timeout time.Duration +} + +func (s *ClamAVStage) Name() string { return "clamav" } + +func (s *ClamAVStage) Run(ctx context.Context, mc *MailContext) *StageResult { + start := time.Now() + result := &StageResult{Stage: s.Name()} + + verdict, detail, err := s.scan(ctx, mc.RawMessage) + result.DurationMs = time.Since(start).Milliseconds() + if err != nil { + result.Result = db.CheckError + result.Detail = "clamd scan failed: " + err.Error() + return result + } + + switch verdict { + case "FOUND": + result.Result = db.CheckFail + result.Score = 100 // malware is always a hard block, not a scored contribution + result.Detail = "malware detected: " + detail + case "OK": + result.Result = db.CheckPass + result.Detail = "clean" + default: + result.Result = db.CheckError + result.Detail = "unexpected clamd response: " + detail + } + return result +} + +func (s *ClamAVStage) scan(ctx context.Context, raw []byte) (verdict, detail string, err error) { + network, address, err := parseClamAddr(s.Addr) + if err != nil { + return "", "", err + } + + dialer := net.Dialer{Timeout: s.Timeout} + conn, err := dialer.DialContext(ctx, network, address) + if err != nil { + return "", "", fmt.Errorf("connecting to clamd: %w", err) + } + defer conn.Close() + if deadline, ok := ctx.Deadline(); ok { + conn.SetDeadline(deadline) + } else if s.Timeout > 0 { + conn.SetDeadline(time.Now().Add(s.Timeout)) + } + + if _, err := conn.Write([]byte("zINSTREAM\x00")); err != nil { + return "", "", fmt.Errorf("sending INSTREAM command: %w", err) + } + + const chunkSize = 8192 + for i := 0; i < len(raw); i += chunkSize { + end := i + chunkSize + if end > len(raw) { + end = len(raw) + } + chunk := raw[i:end] + + lenBuf := make([]byte, 4) + binary.BigEndian.PutUint32(lenBuf, uint32(len(chunk))) + if _, err := conn.Write(lenBuf); err != nil { + return "", "", fmt.Errorf("writing chunk length: %w", err) + } + if _, err := conn.Write(chunk); err != nil { + return "", "", fmt.Errorf("writing chunk data: %w", err) + } + } + if _, err := conn.Write([]byte{0, 0, 0, 0}); err != nil { + return "", "", fmt.Errorf("writing terminator: %w", err) + } + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return "", "", fmt.Errorf("reading clamd response: %w", err) + } + response := strings.TrimRight(string(buf[:n]), "\x00\r\n") + + switch { + case strings.HasSuffix(response, "OK"): + return "OK", response, nil + case strings.Contains(response, "FOUND"): + return "FOUND", response, nil + default: + return "ERROR", response, nil + } +} + +// parseClamAddr accepts "unix:/path/to/socket" or "tcp:host:port" — +// explicit scheme prefix rather than sniffing, so a misconfigured address +// fails loudly at startup instead of guessing wrong. +func parseClamAddr(addr string) (network, address string, err error) { + switch { + case strings.HasPrefix(addr, "unix:"): + return "unix", strings.TrimPrefix(addr, "unix:"), nil + case strings.HasPrefix(addr, "tcp:"): + return "tcp", strings.TrimPrefix(addr, "tcp:"), nil + default: + return "", "", fmt.Errorf("clamav_socket must start with 'unix:' or 'tcp:', got %q", addr) + } +} diff --git a/internal/pipeline/stage_dkim.go b/internal/pipeline/stage_dkim.go new file mode 100644 index 0000000..78ff04e --- /dev/null +++ b/internal/pipeline/stage_dkim.go @@ -0,0 +1,56 @@ +package pipeline + +import ( + "context" + "fmt" + "net" + "strings" + + "gomail/internal/db" + "gomail/internal/dkim" +) + +type DKIMStage struct{} + +func (s *DKIMStage) Name() string { return "dkim" } + +func (s *DKIMStage) Run(ctx context.Context, mc *MailContext) *StageResult { + domain, selector, found := dkim.ExtractSignatureInfo(mc.RawMessage) + if !found { + return &StageResult{Stage: s.Name(), Result: db.CheckWarn, Score: 5, Detail: "no DKIM-Signature header present"} + } + + dnsHost := selector + "._domainkey." + domain + resolver := net.DefaultResolver + txts, err := resolver.LookupTXT(ctx, dnsHost) + if err != nil { + return &StageResult{Stage: s.Name(), Result: db.CheckWarn, Score: 8, + Detail: fmt.Sprintf("DKIM public key DNS lookup failed for %s: %v", dnsHost, err)} + } + + var record string + for _, txt := range txts { + if strings.Contains(txt, "p=") { + record = txt + break + } + } + if record == "" { + return &StageResult{Stage: s.Name(), Result: db.CheckWarn, Score: 8, + Detail: fmt.Sprintf("no DKIM key record found at %s", dnsHost)} + } + + pubDER, err := dkim.ParseDNSPublicKey(record) + if err != nil { + return &StageResult{Stage: s.Name(), Result: db.CheckError, Score: 5, + Detail: fmt.Sprintf("malformed DKIM public key at %s: %v", dnsHost, err)} + } + + if err := dkim.Verify(pubDER, mc.RawMessage); err != nil { + return &StageResult{Stage: s.Name(), Result: db.CheckFail, Score: 20, + Detail: fmt.Sprintf("DKIM signature verification failed (d=%s s=%s): %v", domain, selector, err)} + } + + return &StageResult{Stage: s.Name(), Result: db.CheckPass, Score: 0, + Detail: fmt.Sprintf("DKIM signature valid (d=%s s=%s)", domain, selector)} +} diff --git a/internal/pipeline/stage_dmarc.go b/internal/pipeline/stage_dmarc.go new file mode 100644 index 0000000..86db311 --- /dev/null +++ b/internal/pipeline/stage_dmarc.go @@ -0,0 +1,117 @@ +package pipeline + +import ( + "context" + "fmt" + "net" + "strings" + + "gomail/internal/db" +) + +type DMARCStage struct{} + +func (s *DMARCStage) Name() string { return "dmarc" } + +func (s *DMARCStage) Run(ctx context.Context, mc *MailContext) *StageResult { + msg, err := mc.ParsedMessage() + if err != nil { + return &StageResult{Stage: s.Name(), Result: db.CheckError, Score: 3, + Detail: fmt.Sprintf("could not parse message headers: %v", err)} + } + + fromHeader := msg.Header.Get("From") + fromDomain := extractDomainFromHeader(fromHeader) + if fromDomain == "" { + return &StageResult{Stage: s.Name(), Result: db.CheckWarn, Score: 5, Detail: "could not parse From header domain"} + } + + // Alignment: does the RFC 5322 From domain match (or share an + // organisational domain with) the envelope MAIL FROM domain that SPF + // already checked? Misalignment is exactly what DMARC exists to catch — + // SPF/DKIM passing for a *different* domain than what the user sees in + // their inbox is a classic spoofing pattern. + envelopeDomain := mc.MailFromDomain() + aligned := envelopeDomain != "" && (fromDomain == envelopeDomain || orgDomain(fromDomain) == orgDomain(envelopeDomain)) + + resolver := net.DefaultResolver + txts, err := resolver.LookupTXT(ctx, "_dmarc."+fromDomain) + if err != nil || len(txts) == 0 { + // Fall back to organisational domain per RFC 7489 §6.6.3. A failure + // here just leaves txts empty, handled by the "no record found" + // check below — no separate error path needed. + org := orgDomain(fromDomain) + if org != fromDomain { + txts, _ = resolver.LookupTXT(ctx, "_dmarc."+org) + } + } + + var record string + for _, txt := range txts { + if strings.HasPrefix(strings.ToLower(txt), "v=dmarc1") { + record = txt + break + } + } + if record == "" { + return &StageResult{Stage: s.Name(), Result: db.CheckWarn, Score: 5, + Detail: fmt.Sprintf("no DMARC record published for %s", fromDomain)} + } + + policy := dmarcTag(record, "p") + detail := fmt.Sprintf("DMARC policy=%s for %s, envelope/header alignment=%v", policy, fromDomain, aligned) + + if aligned { + return &StageResult{Stage: s.Name(), Result: db.CheckPass, Score: 0, Detail: detail} + } + + switch policy { + case "reject": + return &StageResult{Stage: s.Name(), Result: db.CheckFail, Score: 25, Detail: detail} + case "quarantine": + return &StageResult{Stage: s.Name(), Result: db.CheckFail, Score: 15, Detail: detail} + default: // "none" or unrecognised + return &StageResult{Stage: s.Name(), Result: db.CheckWarn, Score: 5, Detail: detail} + } +} + +func extractDomainFromHeader(headerValue string) string { + // RFC 5322 From can be "Name " or bare "addr@domain". + addr := headerValue + if i := strings.Index(headerValue, "<"); i >= 0 { + if j := strings.Index(headerValue[i:], ">"); j >= 0 { + addr = headerValue[i+1 : i+j] + } + } + parts := strings.SplitN(strings.ToLower(strings.TrimSpace(addr)), "@", 2) + if len(parts) == 2 { + return parts[1] + } + return "" +} + +// orgDomain approximates the "organisational domain" (RFC 7489 §3.2) by +// taking the last two labels — good enough for common TLDs (.com, .net, +// .org). It does not consult the Public Suffix List, so it will +// mis-identify the org domain for domains under multi-label public suffixes +// like .co.uk; that refinement can be added later without changing the +// stage's shape (it would only affect the fallback DNS lookup and the +// alignment comparison, both isolated to this one helper). +func orgDomain(domain string) string { + labels := strings.Split(domain, ".") + if len(labels) <= 2 { + return domain + } + return strings.Join(labels[len(labels)-2:], ".") +} + +func dmarcTag(record, tag string) string { + for _, part := range strings.Split(record, ";") { + part = strings.TrimSpace(part) + name, value, found := strings.Cut(part, "=") + if found && strings.TrimSpace(name) == tag { + return strings.TrimSpace(value) + } + } + return "" +} diff --git a/internal/pipeline/stage_headers.go b/internal/pipeline/stage_headers.go new file mode 100644 index 0000000..adbfb97 --- /dev/null +++ b/internal/pipeline/stage_headers.go @@ -0,0 +1,80 @@ +package pipeline + +import ( + "fmt" + "context" + "strings" + + "gomail/internal/db" +) + +type HeaderStage struct{} + +func (s *HeaderStage) Name() string { return "headers" } + +func (s *HeaderStage) Run(_ context.Context, mc *MailContext) *StageResult { + msg, err := mc.ParsedMessage() + if err != nil { + return &StageResult{Stage: s.Name(), Result: db.CheckError, Score: 5, + Detail: fmt.Sprintf("could not parse headers: %v", err)} + } + + var issues []string + score := 0.0 + + if msg.Header.Get("From") == "" { + issues = append(issues, "missing From header") + score += 15 + } + if msg.Header.Get("Subject") == "" { + issues = append(issues, "missing Subject header") + score += 3 + } + if msg.Header.Get("Date") == "" { + issues = append(issues, "missing Date header") + score += 5 + } + + fromDomain := extractDomainFromHeader(msg.Header.Get("From")) + envDomain := mc.MailFromDomain() + if fromDomain != "" && envDomain != "" && fromDomain != envDomain { + issues = append(issues, fmt.Sprintf("From header domain (%s) differs from envelope sender (%s)", fromDomain, envDomain)) + score += 10 + } + + if replyTo := msg.Header.Get("Reply-To"); replyTo != "" { + replyDomain := extractDomainFromHeader(replyTo) + if replyDomain != "" && fromDomain != "" && replyDomain != fromDomain { + issues = append(issues, "Reply-To domain differs from From domain") + score += 8 + } + } + + subject := strings.ToLower(msg.Header.Get("Subject")) + urgencyPhrases := []string{ + "urgent", "verify your account", "confirm your", "suspended", + "unusual activity", "act now", "immediately", "security alert", + } + for _, phrase := range urgencyPhrases { + if strings.Contains(subject, phrase) { + issues = append(issues, fmt.Sprintf("urgency language in subject: %q", phrase)) + score += 4 + break + } + } + + result := db.CheckPass + if score > 0 { + result = db.CheckWarn + } + if score >= 20 { + result = db.CheckFail + } + + detail := "no header issues found" + if len(issues) > 0 { + detail = strings.Join(issues, "; ") + } + + return &StageResult{Stage: s.Name(), Result: result, Score: score, Detail: detail} +} diff --git a/internal/pipeline/stage_llm.go b/internal/pipeline/stage_llm.go new file mode 100644 index 0000000..13df58f --- /dev/null +++ b/internal/pipeline/stage_llm.go @@ -0,0 +1,159 @@ +package pipeline + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "gomail/internal/db" +) + +// LLMStage asks a local LLM server for a spam/phishing judgment via the +// OpenAI-compatible /v1/chat/completions endpoint — what llama.cpp's +// server exposes (also what most other local-inference servers converged +// on), so no custom llama.cpp-specific protocol is needed. The model is +// instructed to answer with a single 0-100 integer, parsed directly with +// no JSON-mode/function-calling dependency, since not every local server +// build supports those reliably. +// +// This is deliberately a coarse signal, not a primary verdict: LLM output +// is non-deterministic and shouldn't singlehandedly quarantine mail, so +// its score contribution is capped lower than the deterministic stages +// (SPF/DKIM/DMARC) — see the capping in Run. +type LLMStage struct { + BaseURL string + Model string + Timeout time.Duration +} + +func (s *LLMStage) Name() string { return "llm" } + +const llmSystemPrompt = `You are a spam and phishing classifier. You will be given the headers and ` + + `body of an email. Respond with ONLY a single integer from 0 to 100 representing how likely ` + + `this email is to be spam, phishing, or malicious — 0 means definitely legitimate, 100 means ` + + `definitely malicious. Do not include any other text, explanation, or punctuation in your response.` + +type chatCompletionRequest struct { + Model string `json:"model"` + Messages []chatMessage `json:"messages"` +} + +type chatMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type chatCompletionResponse struct { + Choices []struct { + Message chatMessage `json:"message"` + } `json:"choices"` +} + +// maxScoreContribution caps how much the LLM stage alone can push the +// total score, regardless of what the model returns — see the doc comment +// above for why. +const maxScoreContribution = 30.0 + +func (s *LLMStage) Run(ctx context.Context, mc *MailContext) *StageResult { + start := time.Now() + result := &StageResult{Stage: s.Name()} + + content := mc.RawMessage + const maxContentBytes = 8192 + if len(content) > maxContentBytes { + content = content[:maxContentBytes] + } + + score, err := s.classify(ctx, string(content)) + result.DurationMs = time.Since(start).Milliseconds() + if err != nil { + result.Result = db.CheckError + result.Detail = "LLM classification failed: " + err.Error() + return result + } + + scaledScore := (score / 100.0) * maxScoreContribution + result.Score = scaledScore + result.Detail = fmt.Sprintf("LLM raw score=%.0f/100, capped contribution=%.1f", score, scaledScore) + if score >= 70 { + result.Result = db.CheckFail + } else if score >= 40 { + result.Result = db.CheckWarn + } else { + result.Result = db.CheckPass + } + return result +} + +func (s *LLMStage) classify(ctx context.Context, content string) (float64, error) { + reqBody := chatCompletionRequest{ + Model: s.Model, + Messages: []chatMessage{ + {Role: "system", Content: llmSystemPrompt}, + {Role: "user", Content: content}, + }, + } + bodyJSON, err := json.Marshal(reqBody) + if err != nil { + return 0, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.BaseURL+"/v1/chat/completions", bytes.NewReader(bodyJSON)) + if err != nil { + return 0, fmt.Errorf("building request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: s.Timeout} + resp, err := client.Do(req) + if err != nil { + return 0, fmt.Errorf("request to LLM server: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, fmt.Errorf("LLM server returned status %d", resp.StatusCode) + } + + var result chatCompletionResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return 0, fmt.Errorf("parsing LLM response: %w", err) + } + if len(result.Choices) == 0 { + return 0, fmt.Errorf("LLM response had no choices") + } + + raw := strings.TrimSpace(result.Choices[0].Message.Content) + digits := extractLeadingDigits(raw) + if digits == "" { + return 0, fmt.Errorf("LLM response did not contain a parseable score: %q", raw) + } + score, err := strconv.ParseFloat(digits, 64) + if err != nil { + return 0, fmt.Errorf("parsing score %q: %w", digits, err) + } + if score < 0 { + score = 0 + } + if score > 100 { + score = 100 + } + return score, nil +} + +func extractLeadingDigits(s string) string { + var sb strings.Builder + for _, r := range s { + if r >= '0' && r <= '9' { + sb.WriteRune(r) + } else if sb.Len() > 0 { + break + } + } + return sb.String() +} diff --git a/internal/pipeline/stage_rspamd.go b/internal/pipeline/stage_rspamd.go new file mode 100644 index 0000000..18ed756 --- /dev/null +++ b/internal/pipeline/stage_rspamd.go @@ -0,0 +1,92 @@ +package pipeline + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "gomail/internal/db" +) + +// RspamdStage submits the raw message to rspamd's documented /checkv2 HTTP +// API and maps its score/action into this pipeline's scoring model. No +// third-party rspamd client — a plain POST with the raw RFC 5322 message +// as the body is rspamd's actual documented interface. Off by default — +// only active when config.Pipeline.RspamdURL is set. +type RspamdStage struct { + BaseURL string // e.g. "http://127.0.0.1:11333" + Timeout time.Duration +} + +func (s *RspamdStage) Name() string { return "rspamd" } + +type rspamdResponse struct { + Score float64 `json:"score"` + RequiredScore float64 `json:"required_score"` + Action string `json:"action"` + Symbols map[string]rspamdSymbol `json:"symbols"` +} + +type rspamdSymbol struct { + Score float64 `json:"score"` + Name string `json:"name"` + Options []string `json:"options,omitempty"` +} + +func (s *RspamdStage) Run(ctx context.Context, mc *MailContext) *StageResult { + start := time.Now() + result := &StageResult{Stage: s.Name()} + + resp, err := s.check(ctx, mc.RawMessage) + result.DurationMs = time.Since(start).Milliseconds() + if err != nil { + result.Result = db.CheckError + result.Detail = "rspamd check failed: " + err.Error() + return result + } + + // Translate rspamd's own score onto this pipeline's scale by using its + // score directly — rspamd's score is already meant to be compared + // against thresholds the same way this pipeline's is, so no unit + // conversion trickery, just pass it through. + result.Score = resp.Score + switch resp.Action { + case "reject": + result.Result = db.CheckFail + case "add header", "rewrite subject", "greylist": + result.Result = db.CheckWarn + default: + result.Result = db.CheckPass + } + result.Detail = fmt.Sprintf("rspamd score=%.2f required=%.2f action=%s symbols=%d", + resp.Score, resp.RequiredScore, resp.Action, len(resp.Symbols)) + return result +} + +func (s *RspamdStage) check(ctx context.Context, raw []byte) (*rspamdResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.BaseURL+"/checkv2", bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("building request: %w", err) + } + req.Header.Set("Content-Type", "message/rfc822") + + client := &http.Client{Timeout: s.Timeout} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request to rspamd: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("rspamd returned status %d", resp.StatusCode) + } + + var result rspamdResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("parsing rspamd response: %w", err) + } + return &result, nil +} diff --git a/internal/pipeline/stage_spf.go b/internal/pipeline/stage_spf.go new file mode 100644 index 0000000..d44ced8 --- /dev/null +++ b/internal/pipeline/stage_spf.go @@ -0,0 +1,162 @@ +package pipeline + +import ( + "context" + "fmt" + "net" + "strings" + + "gomail/internal/db" +) + +type SPFStage struct{} + +func (s *SPFStage) Name() string { return "spf" } + +func (s *SPFStage) Run(ctx context.Context, mc *MailContext) *StageResult { + if mc.SenderIP == nil { + return &StageResult{Stage: s.Name(), Result: db.CheckError, Detail: "no sender IP available"} + } + + domain := mc.MailFromDomain() + if domain == "" { + // Null sender (bounces, MAIL FROM:<>) — SPF simply doesn't apply. + return &StageResult{Stage: s.Name(), Result: db.CheckSkipped, Detail: "null sender, SPF not applicable"} + } + + result, detail := checkSPF(ctx, mc.SenderIP, domain) + return &StageResult{Stage: s.Name(), Result: result.check, Score: result.score, Detail: detail} +} + +type spfOutcome struct { + check db.CheckResult + score float64 +} + +func checkSPF(ctx context.Context, senderIP net.IP, domain string) (spfOutcome, string) { + resolver := net.DefaultResolver + txts, err := resolver.LookupTXT(ctx, domain) + if err != nil { + return spfOutcome{db.CheckWarn, 5}, fmt.Sprintf("SPF DNS lookup error for %s: %v", domain, err) + } + + var spfRecord string + for _, txt := range txts { + if strings.HasPrefix(strings.ToLower(txt), "v=spf1") { + spfRecord = txt + break + } + } + if spfRecord == "" { + return spfOutcome{db.CheckWarn, 8}, fmt.Sprintf("no SPF record published for %s", domain) + } + + pass, reason := evaluateSPF(ctx, senderIP, domain, spfRecord, 0) + if pass { + return spfOutcome{db.CheckPass, 0}, fmt.Sprintf("SPF pass for %s (%s)", domain, reason) + } + + switch { + case strings.Contains(spfRecord, "-all"): + return spfOutcome{db.CheckFail, 25}, fmt.Sprintf("SPF hard fail for %s: %s", domain, reason) + case strings.Contains(spfRecord, "~all"): + return spfOutcome{db.CheckWarn, 10}, fmt.Sprintf("SPF softfail for %s: %s", domain, reason) + default: + return spfOutcome{db.CheckWarn, 5}, fmt.Sprintf("SPF neutral/no-match for %s: %s", domain, reason) + } +} + +// evaluateSPF is a pragmatic RFC 7208 evaluator: ip4/ip6/a/mx/include/redirect +// mechanisms, up to 10 levels of recursion (the spec's own limit). It does not +// implement every rarely-used mechanism (ptr, exists) — those are uncommon in +// modern SPF records and can be added later without changing the stage's shape. +func evaluateSPF(ctx context.Context, ip net.IP, domain, record string, depth int) (bool, string) { + if depth > 10 { + return false, "too many SPF redirects/includes" + } + resolver := net.DefaultResolver + + for _, tok := range strings.Fields(record)[1:] { // skip "v=spf1" + lower := strings.ToLower(tok) + switch { + case lower == "+all" || lower == "all": + return true, "all" + case lower == "-all" || lower == "~all" || lower == "?all": + return false, "all (no earlier match)" + + case strings.HasPrefix(lower, "ip4:"), strings.HasPrefix(lower, "ip6:"): + cidr := tok[strings.Index(tok, ":")+1:] + if matchCIDR(ip, cidr) { + return true, "matched " + tok + } + + case strings.HasPrefix(lower, "include:"): + incDomain := tok[len("include:"):] + txts, err := resolver.LookupTXT(ctx, incDomain) + if err == nil { + for _, txt := range txts { + if strings.HasPrefix(strings.ToLower(txt), "v=spf1") { + if ok, r := evaluateSPF(ctx, ip, incDomain, txt, depth+1); ok { + return true, "include:" + incDomain + " -> " + r + } + break + } + } + } + + case lower == "a" || strings.HasPrefix(lower, "a:") || strings.HasPrefix(lower, "a/"): + checkDomain := domain + if strings.HasPrefix(lower, "a:") { + checkDomain = tok[len("a:"):] + } + addrs, err := resolver.LookupHost(ctx, checkDomain) + if err == nil { + for _, a := range addrs { + if net.ParseIP(a).Equal(ip) { + return true, "matched a:" + checkDomain + } + } + } + + case lower == "mx" || strings.HasPrefix(lower, "mx:"): + checkDomain := domain + if strings.HasPrefix(lower, "mx:") { + checkDomain = tok[len("mx:"):] + } + mxs, err := resolver.LookupMX(ctx, checkDomain) + if err == nil { + for _, mx := range mxs { + addrs, _ := resolver.LookupHost(ctx, mx.Host) + for _, a := range addrs { + if net.ParseIP(a).Equal(ip) { + return true, "matched mx:" + checkDomain + } + } + } + } + + case strings.HasPrefix(lower, "redirect="): + redir := tok[len("redirect="):] + txts, err := resolver.LookupTXT(ctx, redir) + if err == nil { + for _, txt := range txts { + if strings.HasPrefix(strings.ToLower(txt), "v=spf1") { + return evaluateSPF(ctx, ip, redir, txt, depth+1) + } + } + } + } + } + return false, "no mechanism matched" +} + +func matchCIDR(ip net.IP, cidr string) bool { + if !strings.Contains(cidr, "/") { + return net.ParseIP(cidr).Equal(ip) + } + _, network, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + return network.Contains(ip) +} diff --git a/internal/pipeline/stage_url.go b/internal/pipeline/stage_url.go new file mode 100644 index 0000000..aece469 --- /dev/null +++ b/internal/pipeline/stage_url.go @@ -0,0 +1,99 @@ +package pipeline + +import ( + "context" + "fmt" + "regexp" + "strings" + + "gomail/internal/db" +) + +type URLStage struct{} + +func (s *URLStage) Name() string { return "urls" } + +var urlRE = regexp.MustCompile(`https?://[^\s<>"']+`) + +var shortenerDomains = []string{ + "bit.ly", "tinyurl.com", "t.co", "goo.gl", "ow.ly", "is.gd", "buff.ly", "short.link", +} + +var suspiciousTLDs = []string{ + ".xyz", ".top", ".click", ".work", ".loan", ".gq", ".tk", ".ml", +} + +func (s *URLStage) Run(_ context.Context, mc *MailContext) *StageResult { + text := string(mc.RawMessage) + urls := urlRE.FindAllString(text, 50) + if len(urls) == 0 { + return &StageResult{Stage: s.Name(), Result: db.CheckPass, Detail: "no URLs found"} + } + + var issues []string + score := 0.0 + seen := map[string]bool{} + + for _, u := range urls { + u = strings.TrimRight(u, ".,;:!?)'\"") + if seen[u] { + continue + } + seen[u] = true + + lower := strings.ToLower(u) + for _, shortener := range shortenerDomains { + if strings.Contains(lower, shortener) { + issues = append(issues, fmt.Sprintf("URL shortener: %s", shortener)) + score += 6 + break + } + } + for _, tld := range suspiciousTLDs { + if strings.Contains(lower, tld) { + issues = append(issues, fmt.Sprintf("suspicious TLD in URL: %s", u)) + score += 4 + break + } + } + // IP-address-literal URLs (http://1.2.3.4/...) are a strong phishing + // signal — legitimate mail almost never links directly to a bare IP. + if ipLiteralRE.MatchString(u) { + issues = append(issues, fmt.Sprintf("IP-literal URL: %s", u)) + score += 8 + } + } + + if score > 30 { + score = 30 // cap — URL heuristics alone shouldn't dominate the verdict + } + + result := db.CheckPass + if score > 0 { + result = db.CheckWarn + } + if score >= 15 { + result = db.CheckFail + } + + detail := fmt.Sprintf("%d unique URL(s) found", len(seen)) + if len(issues) > 0 { + detail += ": " + strings.Join(dedupe(issues), "; ") + } + + return &StageResult{Stage: s.Name(), Result: result, Score: score, Detail: detail} +} + +var ipLiteralRE = regexp.MustCompile(`https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`) + +func dedupe(items []string) []string { + seen := map[string]bool{} + var out []string + for _, i := range items { + if !seen[i] { + seen[i] = true + out = append(out, i) + } + } + return out +} diff --git a/internal/pop3/pop3.go b/internal/pop3/pop3.go new file mode 100644 index 0000000..4adda77 --- /dev/null +++ b/internal/pop3/pop3.go @@ -0,0 +1,520 @@ +// Package pop3 implements a minimal POP3 server (RFC 1939 core commands) +// for legacy clients. Off by default — enabled via config.POP3Config.Enabled. +// USER/PASS/STAT/LIST/RETR/DELE/RSET/NOOP/QUIT/UIDL/TOP — no APOP (requires +// storing plaintext-equivalent passwords, which conflicts with bcrypt-only +// storage) and no PIPELINING negotiation (POP3 has none to negotiate; most +// clients pipeline anyway and this server reads one command per line +// regardless). +package pop3 + +import ( + "bufio" + "context" + "crypto/tls" + "fmt" + "io" + "log/slog" + "net" + "strconv" + "strings" + "sync" + "time" + + "gomail/internal/auth" + "gomail/internal/db" + "gomail/internal/mailstore" + "gomail/internal/ratelimit" +) + +const idleTimeout = 10 * time.Minute + +type Server struct { + database *db.DB + store *mailstore.Store + tlsConf *tls.Config + hostname string + + listeners []net.Listener + wg sync.WaitGroup + sessionWG sync.WaitGroup + + authLimiter *ratelimit.Limiter // per-IP PASS failures/min — checked before credential verification +} + +func NewServer(database *db.DB, store *mailstore.Store, tlsConf *tls.Config, hostname string, authFailuresPerMin int) *Server { + return &Server{ + database: database, + store: store, + tlsConf: tlsConf, + hostname: hostname, + authLimiter: ratelimit.New(authFailuresPerMin), + } +} + +func connHost(addr net.Addr) string { + host, _, err := net.SplitHostPort(addr.String()) + if err != nil { + return addr.String() + } + return host +} + +func (s *Server) ListenAndServe(ctx context.Context, plainAddr, tlsAddr string) error { + specs := []struct { + addr string + useTLS bool + }{ + {plainAddr, false}, + {tlsAddr, true}, + } + + for _, spec := range specs { + ln, err := net.Listen("tcp", spec.addr) + if err != nil { + s.closeAll() + return fmt.Errorf("listen %s: %w", spec.addr, err) + } + if spec.useTLS { + ln = tls.NewListener(ln, s.tlsConf) + } + s.listeners = append(s.listeners, ln) + slog.Info("POP3 listener started", "addr", spec.addr, "implicit_tls", spec.useTLS) + + s.wg.Add(1) + go func(ln net.Listener) { + defer s.wg.Done() + s.acceptLoop(ctx, ln) + }(ln) + } + + <-ctx.Done() + return ctx.Err() +} + +func (s *Server) acceptLoop(ctx context.Context, ln net.Listener) { + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-ctx.Done(): + return + default: + slog.Error("POP3 accept error", "err", err) + return + } + } + s.sessionWG.Add(1) + go func() { + defer s.sessionWG.Done() + sess := newSession(conn, s) + sess.run(ctx) + }() + } +} + +func (s *Server) Shutdown(gracePeriod time.Duration) { + s.closeAll() + done := make(chan struct{}) + go func() { + s.sessionWG.Wait() + close(done) + }() + select { + case <-done: + slog.Info("all POP3 sessions drained cleanly") + case <-time.After(gracePeriod): + slog.Warn("POP3 shutdown grace period expired") + } +} + +func (s *Server) closeAll() { + for _, ln := range s.listeners { + ln.Close() + } + s.wg.Wait() +} + +// ── Session ─────────────────────────────────────────────────────────────────── + +type pop3State int + +const ( + popAuthorization pop3State = iota + popTransaction + popUpdate +) + +type session struct { + conn net.Conn + rw *bufio.ReadWriter + server *Server + + state pop3State + tlsActive bool + user *db.User + pendingUser string // set by USER, consumed by PASS + + // Snapshot of INBOX at login — POP3's message numbers are 1-based indexes + // into this snapshot, exactly like IMAP sequence numbers, and marked + // deleted (not removed) until QUIT commits them in the UPDATE state. + entries []db.MailboxEntry + markedDelete map[int]bool +} + +func newSession(conn net.Conn, server *Server) *session { + _, isTLS := conn.(*tls.Conn) + return &session{ + conn: conn, + rw: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + server: server, + state: popAuthorization, + tlsActive: isTLS, + markedDelete: map[int]bool{}, + } +} + +func (s *session) run(ctx context.Context) { + s.reply(true, fmt.Sprintf("GoMail POP3 server ready (%s)", s.server.hostname)) + + for { + select { + case <-ctx.Done(): + s.reply(false, "server shutting down") + return + default: + } + + s.conn.SetReadDeadline(time.Now().Add(idleTimeout)) + line, err := s.rw.ReadString('\n') + if err != nil { + if err != io.EOF { + slog.Debug("POP3 read error", "err", err) + } + return + } + line = strings.TrimRight(line, "\r\n") + + if !s.dispatch(line) { + return + } + } +} + +func (s *session) dispatch(line string) bool { + parts := strings.SplitN(line, " ", 2) + cmd := strings.ToUpper(parts[0]) + arg := "" + if len(parts) > 1 { + arg = parts[1] + } + + switch cmd { + case "QUIT": + s.commitDeletes() + s.reply(true, "GoMail POP3 server signing off") + return false + case "USER": + s.cmdUser(arg) + case "PASS": + s.cmdPass(arg) + case "STAT": + s.cmdStat() + case "LIST": + s.cmdList(arg) + case "UIDL": + s.cmdUIDL(arg) + case "RETR": + s.cmdRetr(arg) + case "TOP": + s.cmdTop(arg) + case "DELE": + s.cmdDele(arg) + case "RSET": + s.cmdRset() + case "NOOP": + s.reply(true, "") + default: + s.reply(false, "command not recognized") + } + return true +} + +func (s *session) reply(ok bool, msg string) { + prefix := "-ERR" + if ok { + prefix = "+OK" + } + if msg == "" { + s.rw.WriteString(prefix + "\r\n") + } else { + s.rw.WriteString(prefix + " " + msg + "\r\n") + } + s.rw.Flush() +} + +// ── Authorization state ──────────────────────────────────────────────────────── + +func (s *session) cmdUser(arg string) { + if !s.tlsActive { + s.reply(false, "USER over plaintext refused — connect on the implicit-TLS port") + return + } + if s.state != popAuthorization { + s.reply(false, "command not valid in this state") + return + } + s.pendingUser = arg + s.reply(true, "user accepted, send PASS") +} + +func (s *session) cmdPass(arg string) { + if !s.tlsActive { + s.reply(false, "PASS over plaintext refused — connect on the implicit-TLS port") + return + } + + // Checked before attempting any credential verification — same + // rationale as smtp.session.handleAuth's authLimiter check. + ip := connHost(s.conn.RemoteAddr()) + if !s.server.authLimiter.Allow(ip) { + s.reply(false, "too many authentication attempts, try again later") + return + } + + if s.state != popAuthorization || s.pendingUser == "" { + s.reply(false, "USER required first") + return + } + user, ok := auth.Authenticate(s.server.database, s.pendingUser, arg, auth.ScopePOP3) + s.pendingUser = "" + if !ok { + s.reply(false, "authentication failed") + return + } + + entries, err := s.server.database.ListMailboxEntries(user.ID, "INBOX") + if err != nil { + s.reply(false, "temporary error listing mailbox") + return + } + + s.user = user + s.entries = entries + s.state = popTransaction + s.reply(true, fmt.Sprintf("%s's maildrop has %d message(s)", user.Email, len(entries))) +} + +// ── Transaction state ──────────────────────────────────────────────────────── + +func (s *session) cmdStat() { + if !s.requireTransaction() { + return + } + total := int64(0) + count := 0 + for i, e := range s.entries { + if s.markedDelete[i+1] { + continue + } + total += e.SizeBytes + count++ + } + s.reply(true, fmt.Sprintf("%d %d", count, total)) +} + +func (s *session) cmdList(arg string) { + if !s.requireTransaction() { + return + } + if arg != "" { + n, err := strconv.Atoi(arg) + if err != nil || n < 1 || n > len(s.entries) || s.markedDelete[n] { + s.reply(false, "no such message") + return + } + s.reply(true, fmt.Sprintf("%d %d", n, s.entries[n-1].SizeBytes)) + return + } + + s.reply(true, fmt.Sprintf("%d messages", s.liveCount())) + for i, e := range s.entries { + if s.markedDelete[i+1] { + continue + } + s.rw.WriteString(fmt.Sprintf("%d %d\r\n", i+1, e.SizeBytes)) + } + s.rw.WriteString(".\r\n") + s.rw.Flush() +} + +func (s *session) cmdUIDL(arg string) { + if !s.requireTransaction() { + return + } + if arg != "" { + n, err := strconv.Atoi(arg) + if err != nil || n < 1 || n > len(s.entries) || s.markedDelete[n] { + s.reply(false, "no such message") + return + } + s.reply(true, fmt.Sprintf("%d %s", n, s.entries[n-1].ID)) + return + } + + s.reply(true, "unique-id listing follows") + for i, e := range s.entries { + if s.markedDelete[i+1] { + continue + } + s.rw.WriteString(fmt.Sprintf("%d %s\r\n", i+1, e.ID)) + } + s.rw.WriteString(".\r\n") + s.rw.Flush() +} + +func (s *session) cmdRetr(arg string) { + if !s.requireTransaction() { + return + } + n, ok := s.validMessageNum(arg) + if !ok { + return + } + entry := s.entries[n-1] + raw, err := s.server.store.Read(entry.EMLPath) + if err != nil { + s.reply(false, "error reading message") + return + } + s.reply(true, fmt.Sprintf("%d octets", len(raw))) + s.writeDotStuffed(raw) +} + +func (s *session) cmdTop(arg string) { + if !s.requireTransaction() { + return + } + parts := strings.SplitN(arg, " ", 2) + if len(parts) != 2 { + s.reply(false, "TOP requires message number and line count") + return + } + n, ok := s.validMessageNum(parts[0]) + if !ok { + return + } + nLines, err := strconv.Atoi(parts[1]) + if err != nil || nLines < 0 { + s.reply(false, "invalid line count") + return + } + + entry := s.entries[n-1] + raw, err := s.server.store.Read(entry.EMLPath) + if err != nil { + s.reply(false, "error reading message") + return + } + + headerEnd := strings.Index(string(raw), "\r\n\r\n") + var header, body string + if headerEnd >= 0 { + header = string(raw[:headerEnd+4]) + body = string(raw[headerEnd+4:]) + } else { + header = string(raw) + } + + bodyLines := strings.Split(body, "\r\n") + if nLines > len(bodyLines) { + nLines = len(bodyLines) + } + result := header + strings.Join(bodyLines[:nLines], "\r\n") + + s.reply(true, "top of message follows") + s.writeDotStuffed([]byte(result)) +} + +func (s *session) cmdDele(arg string) { + if !s.requireTransaction() { + return + } + n, ok := s.validMessageNum(arg) + if !ok { + return + } + s.markedDelete[n] = true + s.reply(true, fmt.Sprintf("message %d marked for deletion", n)) +} + +func (s *session) cmdRset() { + if !s.requireTransaction() { + return + } + s.markedDelete = map[int]bool{} + s.reply(true, "maildrop state reset") +} + +// commitDeletes runs at QUIT — actually removes messages marked with DELE, +// per RFC 1939 §5's UPDATE state semantics (deletion is provisional until +// QUIT; RSET or a dropped connection discards the marks instead). +func (s *session) commitDeletes() { + if s.state != popTransaction { + return + } + for i, e := range s.entries { + if s.markedDelete[i+1] { + s.server.database.DeleteMailboxEntry(e.ID) + } + } + s.state = popUpdate +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +func (s *session) requireTransaction() bool { + if s.state != popTransaction { + s.reply(false, "command not valid in this state") + return false + } + return true +} + +func (s *session) validMessageNum(arg string) (int, bool) { + n, err := strconv.Atoi(arg) + if err != nil || n < 1 || n > len(s.entries) { + s.reply(false, "no such message") + return 0, false + } + if s.markedDelete[n] { + s.reply(false, "message already deleted") + return 0, false + } + return n, true +} + +func (s *session) liveCount() int { + c := 0 + for i := range s.entries { + if !s.markedDelete[i+1] { + c++ + } + } + return c +} + +// writeDotStuffed writes a message body with byte-stuffing (a line starting +// with "." gets an extra "." prepended) and the terminating "." line, per +// RFC 1939 §3. +func (s *session) writeDotStuffed(raw []byte) { + lines := strings.Split(string(raw), "\r\n") + for _, line := range lines { + if strings.HasPrefix(line, ".") { + s.rw.WriteString("." + line + "\r\n") + } else { + s.rw.WriteString(line + "\r\n") + } + } + s.rw.WriteString(".\r\n") + s.rw.Flush() +} diff --git a/internal/queue/queue.go b/internal/queue/queue.go new file mode 100644 index 0000000..d3cd1fe --- /dev/null +++ b/internal/queue/queue.go @@ -0,0 +1,334 @@ +// Package queue implements the outbound delivery worker: polls due entries, +// resolves MX records, delivers via net/smtp (stdlib), and handles retry +// backoff and bounce generation for permanent failures. +package queue + +import ( + "crypto/tls" + "fmt" + "log/slog" + "net" + "net/smtp" + "strings" + "time" + + "gomail/internal/db" + "gomail/internal/dkim" + "gomail/internal/mailstore" +) + +const ( + maxAttempts = 5 + pollInterval = 30 * time.Second + deliveryTimeout = 60 * time.Second +) + +// Deliverer is the interface the worker uses to actually hand a message to a +// remote MTA — abstracted so tests can inject a fake without real network +// access (outbound port 25 is blocked in most sandboxed/dev environments). +type Deliverer interface { + Deliver(from, to string, raw []byte) error +} + +// KeyLookup resolves the DKIM signing key for a sending domain, returning +// (privateKeyPEM, selector, found). The worker calls this fresh on every +// delivery attempt (not cached at startup) so key rotation via the admin +// portal takes effect immediately without a restart. +type KeyLookup func(fromDomain string) (privateKeyPEM []byte, selector string, ok bool) + +// Worker polls outbound_queue and processes due entries. +type Worker struct { + database *db.DB + store *mailstore.Store + deliverer Deliverer + keyLookup KeyLookup + stopCh chan struct{} +} + +func NewWorker(database *db.DB, store *mailstore.Store) *Worker { + return &Worker{ + database: database, + store: store, + deliverer: &MXDeliverer{Hostname: "gomail"}, + stopCh: make(chan struct{}), + } +} + +// WithDeliverer overrides the delivery mechanism — used by tests. +func (w *Worker) WithDeliverer(d Deliverer) *Worker { + w.deliverer = d + return w +} + +// WithKeyLookup enables DKIM signing before every delivery attempt. Signing +// happens here in the worker — not inside a specific Deliverer implementation +// — so it applies uniformly regardless of transport (MX delivery, a test +// fake, or any future alternative). +func (w *Worker) WithKeyLookup(kl KeyLookup) *Worker { + w.keyLookup = kl + return w +} + +// Run starts the polling loop. Blocks until Stop is called. +func (w *Worker) Run() { + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + slog.Info("outbound queue worker started", "poll_interval", pollInterval) + w.ProcessOnce() // run immediately on start, don't wait for the first tick + + for { + select { + case <-w.stopCh: + return + case <-ticker.C: + w.ProcessOnce() + } + } +} + +func (w *Worker) Stop() { + close(w.stopCh) +} + +// ProcessOnce runs a single pass: attempts delivery for all due entries, +// then bounces anything that has exhausted its retry budget. +func (w *Worker) ProcessOnce() { + entries, err := w.database.DueOutboundEntries(maxAttempts, 100) + if err != nil { + slog.Error("queue: failed to load due entries", "err", err) + return + } + + for _, entry := range entries { + w.attemptDelivery(entry) + } + + failed, err := w.database.PermanentlyFailedEntries(maxAttempts) + if err != nil { + slog.Error("queue: failed to load permanently failed entries", "err", err) + return + } + for _, entry := range failed { + w.bounce(entry) + } +} + +func (w *Worker) attemptDelivery(entry db.OutboundQueueEntry) { + raw, err := w.store.Read(entry.EMLPath) + if err != nil { + slog.Error("queue: failed to read queued message", "id", entry.ID, "err", err) + w.scheduleRetry(entry, fmt.Sprintf("read failed: %v", err)) + return + } + + if w.keyLookup != nil { + fromDomain := domainOf(entry.FromAddress) + if privateKeyPEM, selector, ok := w.keyLookup(fromDomain); ok { + signed, err := dkim.Sign(privateKeyPEM, fromDomain, selector, raw) + if err != nil { + slog.Warn("queue: DKIM signing failed, sending unsigned", "domain", fromDomain, "err", err) + } else { + raw = signed + } + } + } + + err = w.deliverer.Deliver(entry.FromAddress, entry.ToAddress, raw) + if err == nil { + slog.Info("queue: delivered", "to", entry.ToAddress, "attempts", entry.Attempts+1) + if delErr := w.database.DeleteOutboundEntry(entry.ID); delErr != nil { + slog.Error("queue: failed to delete completed entry", "err", delErr) + } + return + } + + if isPermanentError(err) { + slog.Warn("queue: permanent delivery failure, will bounce", "to", entry.ToAddress, "err", err) + // Fast-forward attempts to the max so the next ProcessOnce pass bounces + // it immediately, instead of waiting through the full retry schedule. + remaining := maxAttempts - entry.Attempts + for i := 0; i < remaining; i++ { + w.database.RetryOutboundEntry(entry.ID, time.Now().UTC(), err.Error()) + } + return + } + + slog.Info("queue: temporary delivery failure, will retry", "to", entry.ToAddress, "attempt", entry.Attempts+1, "err", err) + w.scheduleRetry(entry, err.Error()) +} + +func (w *Worker) scheduleRetry(entry db.OutboundQueueEntry, errMsg string) { + backoff := backoffDuration(entry.Attempts + 1) + next := time.Now().UTC().Add(backoff) + if err := w.database.RetryOutboundEntry(entry.ID, next, errMsg); err != nil { + slog.Error("queue: failed to schedule retry", "err", err) + } +} + +// backoffDuration implements exponential backoff: 5m, 20m, 1h20m, 5h20m, ~21h +// for attempts 1 through 5, capping the total retry window near 5 days as +// planned (RFC 5321 recommends retrying for at least 4-5 days before giving up). +func backoffDuration(attempt int) time.Duration { + base := 5 * time.Minute + d := base + for i := 1; i < attempt; i++ { + d *= 4 + } + max := 24 * time.Hour + if d > max { + d = max + } + return d +} + +// bounce generates a DSN-style bounce message and delivers it to the local +// sender's INBOX (the original MAIL FROM on submission is always a local +// user, since session.go enforces that match at RCPT TO time). +func (w *Worker) bounce(entry db.OutboundQueueEntry) { + user, err := w.database.LookupUserByEmail(entry.FromAddress) + if err != nil { + slog.Error("queue: cannot bounce — original sender not found locally", "from", entry.FromAddress, "err", err) + w.database.DeleteOutboundEntry(entry.ID) + return + } + + bounceBody := fmt.Sprintf( + "From: Mail Delivery System \r\n"+ + "To: %s\r\n"+ + "Subject: Undelivered Mail Returned to Sender\r\n"+ + "Date: %s\r\n"+ + "\r\n"+ + "This is an automatically generated Delivery Status Notification.\r\n\r\n"+ + "Delivery to the following recipient failed permanently after %d attempts:\r\n\r\n"+ + " %s\r\n\r\n"+ + "Last error: %s\r\n\r\n"+ + "This is the final notification; no further attempts will be made.\r\n", + domainOf(entry.FromAddress), entry.FromAddress, time.Now().UTC().Format(time.RFC1123Z), + entry.Attempts, entry.ToAddress, entry.LastError, + ) + + if _, err := w.store.Deliver(user.ID, user.Email, "INBOX", []byte(bounceBody)); err != nil { + slog.Error("queue: failed to deliver bounce", "err", err) + return + } + + slog.Info("queue: bounce delivered", "to", entry.FromAddress, "original_recipient", entry.ToAddress) + w.database.DeleteOutboundEntry(entry.ID) +} + +func domainOf(email string) string { + parts := strings.SplitN(email, "@", 2) + if len(parts) == 2 { + return parts[1] + } + return "localhost" +} + +// isPermanentError distinguishes 5xx (permanent) from 4xx/network (temporary) +// SMTP failures — net/smtp wraps the server's textual response in the error, +// so we inspect it for the leading status code digit. +func isPermanentError(err error) bool { + msg := err.Error() + // net/smtp errors look like "553 5.1.1 User unknown" when they come from + // the remote server's response. + for _, code := range []string{"550", "551", "552", "553", "554"} { + if strings.Contains(msg, code) { + return true + } + } + return false +} + +// ── MX-resolving deliverer (stdlib net/smtp + net.LookupMX) ──────────────────── + +// MXDeliverer is the real production Deliverer: resolves the recipient +// domain's MX records, connects (with STARTTLS if offered), and hands off +// via net/smtp — Go's standard library SMTP client, chosen specifically to +// stay dependency-free for outbound delivery just as the inbound server is +// hand-rolled from net.Listener. Pure transport — DKIM signing (if any) +// happens in Worker.attemptDelivery before Deliver is called, so it applies +// uniformly regardless of which Deliverer implementation is in use. +type MXDeliverer struct { + Hostname string // EHLO identity +} + +func (d *MXDeliverer) Deliver(from, to string, raw []byte) error { + domain := domainOf(to) + mxHosts, err := lookupMXHosts(domain) + if err != nil { + return fmt.Errorf("451 4.4.3 MX lookup failed for %s: %w", domain, err) + } + + var lastErr error + for _, host := range mxHosts { + if err := d.deliverToHost(host, from, to, raw); err != nil { + lastErr = err + continue + } + return nil + } + return lastErr +} + +func (d *MXDeliverer) deliverToHost(host, from, to string, raw []byte) error { + conn, err := net.DialTimeout("tcp", host+":25", deliveryTimeout) + if err != nil { + return fmt.Errorf("421 4.4.1 connect to %s failed: %w", host, err) + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(deliveryTimeout)) + + client, err := smtp.NewClient(conn, host) + if err != nil { + return fmt.Errorf("421 4.4.1 SMTP handshake with %s failed: %w", host, err) + } + defer client.Close() + + if err := client.Hello(d.Hostname); err != nil { + return fmt.Errorf("EHLO to %s failed: %w", host, err) + } + + if ok, _ := client.Extension("STARTTLS"); ok { + tlsConf := &tls.Config{ServerName: host, MinVersion: tls.VersionTLS12} + if err := client.StartTLS(tlsConf); err != nil { + slog.Warn("STARTTLS failed, continuing without encryption", "host", host, "err", err) + } + } + + if err := client.Mail(from); err != nil { + return err // preserves the remote server's status code in the error text + } + if err := client.Rcpt(to); err != nil { + return err + } + + w, err := client.Data() + if err != nil { + return err + } + if _, err := w.Write(raw); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + + return client.Quit() +} + +func lookupMXHosts(domain string) ([]string, error) { + mxs, err := net.LookupMX(domain) + if err != nil || len(mxs) == 0 { + // RFC 5321 §5.1 fallback: if no MX records, try the domain's A record directly. + if _, aErr := net.LookupHost(domain); aErr == nil { + return []string{domain}, nil + } + return nil, fmt.Errorf("no MX or A record for %s: %w", domain, err) + } + hosts := make([]string, len(mxs)) + for i, mx := range mxs { + hosts[i] = strings.TrimSuffix(mx.Host, ".") + } + return hosts, nil +} diff --git a/internal/ratelimit/http.go b/internal/ratelimit/http.go new file mode 100644 index 0000000..8b53b38 --- /dev/null +++ b/internal/ratelimit/http.go @@ -0,0 +1,42 @@ +package ratelimit + +import ( + "net" + "net/http" + "strings" +) + +// HTTPMiddleware wraps next with per-client-IP rate limiting, returning 429 +// for requests over the limit. realIPHeader (e.g. "X-Forwarded-For"), if +// non-empty, is trusted for the client IP instead of RemoteAddr — only set +// this when the server is genuinely behind a reverse proxy that sets it; +// trusting it otherwise lets any client spoof their rate-limit identity. +func (l *Limiter) HTTPMiddleware(realIPHeader string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip := ClientIP(r, realIPHeader) + if !l.Allow(ip) { + w.Header().Set("Retry-After", "60") + http.Error(w, "rate limit exceeded, try again shortly", http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, r) + }) +} + +// ClientIP resolves the request's client IP the same way HTTPMiddleware +// does, for callers that need it outside a rate-limit context (e.g. an IP +// allowlist middleware). See HTTPMiddleware's doc comment for the +// realIPHeader trust caveat. +func ClientIP(r *http.Request, realIPHeader string) string { + if realIPHeader != "" { + if v := r.Header.Get(realIPHeader); v != "" { + parts := strings.Split(v, ",") + return strings.TrimSpace(parts[0]) + } + } + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return host +} diff --git a/internal/ratelimit/ratelimit.go b/internal/ratelimit/ratelimit.go new file mode 100644 index 0000000..5d07f78 --- /dev/null +++ b/internal/ratelimit/ratelimit.go @@ -0,0 +1,109 @@ +// Package ratelimit implements a per-key token-bucket rate limiter — no +// third-party rate-limiting library. A token bucket (rather than a hard +// fixed-window reset) is used deliberately: it smooths out bursts at +// window boundaries that a naive "reset every 60s" counter would allow +// (e.g. 20 requests at 0:59 plus another 20 at 1:01 both passing a +// "20/min" limit reset at the minute boundary). Safe for concurrent use. +package ratelimit + +import ( + "sync" + "time" +) + +type bucket struct { + tokens float64 + lastRefill time.Time +} + +// Limiter enforces "at most ratePerMinute events per key, per minute" with +// burst tolerance up to ratePerMinute tokens banked at once (i.e. a key +// that's been idle can burst up to the full per-minute allowance instantly, +// then is throttled to the steady-state rate — standard token-bucket +// behavior, not a stricter "evenly spaced" enforcement). +type Limiter struct { + ratePerMinute float64 + mu sync.Mutex + buckets map[string]*bucket + + stopCleanup chan struct{} +} + +// New creates a limiter allowing ratePerMinute events per key. Pass 0 to +// disable limiting entirely (Allow always returns true) — this is how a +// zero/unset config value opts a listener out of rate limiting rather than +// silently blocking everything. +func New(ratePerMinute int) *Limiter { + l := &Limiter{ + ratePerMinute: float64(ratePerMinute), + buckets: make(map[string]*bucket), + stopCleanup: make(chan struct{}), + } + if ratePerMinute > 0 { + go l.cleanupLoop() + } + return l +} + +// Allow reports whether an event for key is permitted right now, consuming +// one token if so. Safe to call from many goroutines concurrently. +func (l *Limiter) Allow(key string) bool { + if l.ratePerMinute <= 0 { + return true + } + + l.mu.Lock() + defer l.mu.Unlock() + + now := time.Now() + b, ok := l.buckets[key] + if !ok { + b = &bucket{tokens: l.ratePerMinute - 1, lastRefill: now} + l.buckets[key] = b + return true + } + + elapsed := now.Sub(b.lastRefill).Seconds() + refill := elapsed * (l.ratePerMinute / 60.0) + b.tokens += refill + if b.tokens > l.ratePerMinute { + b.tokens = l.ratePerMinute + } + b.lastRefill = now + + if b.tokens < 1 { + return false + } + b.tokens-- + return true +} + +// cleanupLoop periodically evicts buckets idle long enough to have fully +// refilled, so a limiter tracking many distinct one-off IPs doesn't grow +// unboundedly over a long-running server's lifetime. +func (l *Limiter) cleanupLoop() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + for { + select { + case <-l.stopCleanup: + return + case <-ticker.C: + l.mu.Lock() + now := time.Now() + for key, b := range l.buckets { + if now.Sub(b.lastRefill) > 30*time.Minute { + delete(l.buckets, key) + } + } + l.mu.Unlock() + } + } +} + +// Stop releases the background cleanup goroutine. +func (l *Limiter) Stop() { + if l.ratePerMinute > 0 { + close(l.stopCleanup) + } +} diff --git a/internal/sieve/interp.go b/internal/sieve/interp.go new file mode 100644 index 0000000..4d9dac7 --- /dev/null +++ b/internal/sieve/interp.go @@ -0,0 +1,105 @@ +package sieve + +import "strings" + +// Result is the outcome of running a script against one message. +type Result struct { + Action string // "fileinto" | "discard" | "keep" (default if nothing else fired) + Folder string // set only when Action == "fileinto" +} + +// Execute runs script against the given headers (case-insensitive header +// names, matching real email header semantics) and returns the first +// decisive action encountered. "stop" halts execution immediately with +// whatever result has accumulated so far. If no action fires, the default +// result is "keep" (deliver to INBOX), matching RFC 5228 §2.10's implicit +// keep behavior. +// +// Simplification: real Sieve treats keep/fileinto/discard as an +// accumulating SET of actions (a message can be filed into a folder AND +// kept in INBOX, for instance) — this implementation tracks only the single +// most recent action instead, last-one-wins. This still matches RFC 5228's +// core rule that "discard cancels the implicit keep, but an explicit keep +// after it still delivers" (§4.4) — a discard followed by an unconditional +// keep with no stop in between DOES deliver the message, correctly. What's +// NOT supported is a script that intends both fileinto AND keep to fire +// simultaneously (message copied to a folder AND left in INBOX) — write +// "stop;" after the decisive action if that's not the intended behavior, +// same as real Sieve authors are advised to do to avoid ambiguity. +func Execute(script *Script, headers map[string]string) Result { + result := Result{Action: "keep"} + execStatements(script.Statements, headers, &result) + return result +} + +// execStatements returns true if execution should stop (a "stop" action fired). +func execStatements(stmts []Statement, headers map[string]string, result *Result) bool { + for _, stmt := range stmts { + switch s := stmt.(type) { + case Action: + switch s.Name { + case "fileinto": + result.Action = "fileinto" + result.Folder = s.Arg + case "discard": + result.Action = "discard" + case "keep": + result.Action = "keep" + case "stop": + return true + } + case IfStatement: + if evalTest(s.Test, headers) { + if execStatements(s.Then, headers, result) { + return true + } + continue + } + matched := false + for _, ei := range s.ElseIfs { + if evalTest(ei.Test, headers) { + matched = true + if execStatements(ei.Then, headers, result) { + return true + } + break + } + } + if !matched && s.HasElse { + if execStatements(s.Else, headers, result) { + return true + } + } + } + } + return false +} + +func evalTest(t Test, headers map[string]string) bool { + switch t.Kind { + case "true": + return true + case "header": + actual, ok := lookupHeader(headers, t.Header) + if !ok { + return false + } + switch t.MatchType { + case "contains": + return strings.Contains(strings.ToLower(actual), strings.ToLower(t.Value)) + case "is": + return strings.EqualFold(strings.TrimSpace(actual), strings.TrimSpace(t.Value)) + } + } + return false +} + +func lookupHeader(headers map[string]string, name string) (string, bool) { + // Case-insensitive lookup — email headers are case-insensitive per RFC 5322. + for k, v := range headers { + if strings.EqualFold(k, name) { + return v, true + } + } + return "", false +} diff --git a/internal/sieve/lexer.go b/internal/sieve/lexer.go new file mode 100644 index 0000000..64d2d8b --- /dev/null +++ b/internal/sieve/lexer.go @@ -0,0 +1,131 @@ +// Package sieve implements a Sieve (RFC 5228) interpreter covering the +// common mail-filtering subset: header tests (:contains, :is), if/elsif/else, +// and the fileinto/discard/keep/stop actions. Not full RFC 5228 — no +// extensions (vacation, reject, notify), no envelope/size/address tests, +// no allof/anyof boolean combinators. This covers what real users actually +// write for "move mail matching X to folder Y" / "discard mail from Z", +// which is the overwhelming majority of real-world Sieve scripts; broader +// grammar support is a natural follow-up once client compatibility testing +// calls for it. +package sieve + +import ( + "fmt" + "strings" + "unicode" +) + +type tokenKind int + +const ( + tokIdent tokenKind = iota + tokString + tokTag // :contains, :is, etc. + tokSemicolon + tokLBrace + tokRBrace + tokEOF +) + +type token struct { + kind tokenKind + value string +} + +type lexer struct { + input []rune + pos int +} + +func newLexer(script string) *lexer { + return &lexer{input: []rune(script)} +} + +func (l *lexer) next() (token, error) { + l.skipWhitespaceAndComments() + if l.pos >= len(l.input) { + return token{kind: tokEOF}, nil + } + + c := l.input[l.pos] + switch { + case c == ';': + l.pos++ + return token{kind: tokSemicolon, value: ";"}, nil + case c == '{': + l.pos++ + return token{kind: tokLBrace, value: "{"}, nil + case c == '}': + l.pos++ + return token{kind: tokRBrace, value: "}"}, nil + case c == '"': + return l.readString() + case c == ':': + return l.readTag() + case unicode.IsLetter(c): + return l.readIdent() + default: + return token{}, fmt.Errorf("unexpected character %q at position %d", c, l.pos) + } +} + +func (l *lexer) skipWhitespaceAndComments() { + for l.pos < len(l.input) { + c := l.input[l.pos] + if unicode.IsSpace(c) { + l.pos++ + continue + } + // Single-line comment: # ... end of line + if c == '#' { + for l.pos < len(l.input) && l.input[l.pos] != '\n' { + l.pos++ + } + continue + } + // Bracketed comment: /* ... */ + if c == '/' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '*' { + l.pos += 2 + for l.pos+1 < len(l.input) && !(l.input[l.pos] == '*' && l.input[l.pos+1] == '/') { + l.pos++ + } + l.pos += 2 + continue + } + break + } +} + +func (l *lexer) readString() (token, error) { + l.pos++ // skip opening quote + var sb strings.Builder + for l.pos < len(l.input) && l.input[l.pos] != '"' { + if l.input[l.pos] == '\\' && l.pos+1 < len(l.input) { + l.pos++ + } + sb.WriteRune(l.input[l.pos]) + l.pos++ + } + if l.pos >= len(l.input) { + return token{}, fmt.Errorf("unterminated string literal") + } + l.pos++ // skip closing quote + return token{kind: tokString, value: sb.String()}, nil +} + +func (l *lexer) readTag() (token, error) { + start := l.pos + l.pos++ // skip ':' + for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || l.input[l.pos] == '-') { + l.pos++ + } + return token{kind: tokTag, value: string(l.input[start:l.pos])}, nil +} + +func (l *lexer) readIdent() (token, error) { + start := l.pos + for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '_') { + l.pos++ + } + return token{kind: tokIdent, value: string(l.input[start:l.pos])}, nil +} diff --git a/internal/sieve/parser.go b/internal/sieve/parser.go new file mode 100644 index 0000000..36e7b94 --- /dev/null +++ b/internal/sieve/parser.go @@ -0,0 +1,229 @@ +package sieve + +import "fmt" + +// ── AST ─────────────────────────────────────────────────────────────────────── + +type Script struct { + Statements []Statement +} + +// Statement is either an Action or an IfStatement. +type Statement interface{ isStatement() } + +type Action struct { + Name string // "fileinto" | "discard" | "keep" | "stop" + Arg string // folder name for fileinto, empty otherwise +} + +func (Action) isStatement() {} + +type IfStatement struct { + Test Test + Then []Statement + ElseIfs []ElseIf + Else []Statement + HasElse bool +} + +func (IfStatement) isStatement() {} + +type ElseIf struct { + Test Test + Then []Statement +} + +// Test is a condition — this pass supports only header tests, the +// overwhelming majority of real-world filtering rules. +type Test struct { + Kind string // "header" | "true" + MatchType string // "contains" | "is" + Header string + Value string +} + +// ── Parser ──────────────────────────────────────────────────────────────────── + +type parser struct { + lex *lexer + cur token +} + +func Parse(script string) (*Script, error) { + p := &parser{lex: newLexer(script)} + if err := p.advance(); err != nil { + return nil, err + } + + s := &Script{} + for p.cur.kind != tokEOF { + stmt, err := p.parseStatement() + if err != nil { + return nil, err + } + s.Statements = append(s.Statements, stmt) + } + return s, nil +} + +func (p *parser) advance() error { + t, err := p.lex.next() + if err != nil { + return err + } + p.cur = t + return nil +} + +func (p *parser) expect(kind tokenKind, desc string) (token, error) { + if p.cur.kind != kind { + return token{}, fmt.Errorf("expected %s, got %q", desc, p.cur.value) + } + t := p.cur + if err := p.advance(); err != nil { + return token{}, err + } + return t, nil +} + +func (p *parser) parseStatement() (Statement, error) { + if p.cur.kind != tokIdent { + return nil, fmt.Errorf("expected statement, got %q", p.cur.value) + } + + switch p.cur.value { + case "if": + return p.parseIf() + case "fileinto": + if err := p.advance(); err != nil { + return nil, err + } + arg, err := p.expect(tokString, "folder name") + if err != nil { + return nil, err + } + if _, err := p.expect(tokSemicolon, ";"); err != nil { + return nil, err + } + return Action{Name: "fileinto", Arg: arg.value}, nil + case "discard", "keep", "stop": + name := p.cur.value + if err := p.advance(); err != nil { + return nil, err + } + if _, err := p.expect(tokSemicolon, ";"); err != nil { + return nil, err + } + return Action{Name: name}, nil + default: + return nil, fmt.Errorf("unsupported command %q", p.cur.value) + } +} + +func (p *parser) parseIf() (Statement, error) { + if err := p.advance(); err != nil { // skip "if" + return nil, err + } + test, err := p.parseTest() + if err != nil { + return nil, err + } + then, err := p.parseBlock() + if err != nil { + return nil, err + } + + stmt := IfStatement{Test: test, Then: then} + + for p.cur.kind == tokIdent && p.cur.value == "elsif" { + if err := p.advance(); err != nil { + return nil, err + } + elifTest, err := p.parseTest() + if err != nil { + return nil, err + } + elifThen, err := p.parseBlock() + if err != nil { + return nil, err + } + stmt.ElseIfs = append(stmt.ElseIfs, ElseIf{Test: elifTest, Then: elifThen}) + } + + if p.cur.kind == tokIdent && p.cur.value == "else" { + if err := p.advance(); err != nil { + return nil, err + } + elseBlock, err := p.parseBlock() + if err != nil { + return nil, err + } + stmt.Else = elseBlock + stmt.HasElse = true + } + + return stmt, nil +} + +func (p *parser) parseTest() (Test, error) { + if p.cur.kind != tokIdent { + return Test{}, fmt.Errorf("expected test, got %q", p.cur.value) + } + + if p.cur.value == "true" { + if err := p.advance(); err != nil { + return Test{}, err + } + return Test{Kind: "true"}, nil + } + + if p.cur.value != "header" { + return Test{}, fmt.Errorf("unsupported test %q (only 'header' and 'true' supported)", p.cur.value) + } + if err := p.advance(); err != nil { + return Test{}, err + } + + if p.cur.kind != tokTag { + return Test{}, fmt.Errorf("expected match type (:contains or :is), got %q", p.cur.value) + } + matchType := p.cur.value[1:] // strip leading ':' + if matchType != "contains" && matchType != "is" { + return Test{}, fmt.Errorf("unsupported match type %q (only :contains and :is supported)", matchType) + } + if err := p.advance(); err != nil { + return Test{}, err + } + + headerTok, err := p.expect(tokString, "header name") + if err != nil { + return Test{}, err + } + valueTok, err := p.expect(tokString, "match value") + if err != nil { + return Test{}, err + } + + return Test{Kind: "header", MatchType: matchType, Header: headerTok.value, Value: valueTok.value}, nil +} + +func (p *parser) parseBlock() ([]Statement, error) { + if _, err := p.expect(tokLBrace, "{"); err != nil { + return nil, err + } + var stmts []Statement + for p.cur.kind != tokRBrace { + if p.cur.kind == tokEOF { + return nil, fmt.Errorf("unterminated block, expected }") + } + stmt, err := p.parseStatement() + if err != nil { + return nil, err + } + stmts = append(stmts, stmt) + } + if _, err := p.expect(tokRBrace, "}"); err != nil { + return nil, err + } + return stmts, nil +} diff --git a/internal/sieve/sieve_fuzz_test.go b/internal/sieve/sieve_fuzz_test.go new file mode 100644 index 0000000..4943184 --- /dev/null +++ b/internal/sieve/sieve_fuzz_test.go @@ -0,0 +1,32 @@ +package sieve + +import "testing" + +func FuzzParse(f *testing.F) { + f.Add(`if header :contains "subject" "invoice" { fileinto "Invoices"; stop; }`) + f.Add(`if header :is "from" "boss@example.com" { fileinto "Important"; } elsif header :contains "subject" "urgent" { fileinto "Important"; } else { keep; }`) + f.Add("") + f.Add("keep;") + f.Add("if true { discard; }") + f.Add(`if header :contains "subject" { fileinto "X" }`) + f.Add("if header { }") + f.Add("{{{{{{{") + f.Add(`if header :contains "a" "b`) + f.Add("if header :bogus \"x\" \"y\" { keep; }") + f.Add("fileinto;") + + f.Fuzz(func(t *testing.T, data string) { + // This is the fuzz target most directly exposed to untrusted input + // in production — every ManageSieve PUTSCRIPT is parsed by this + // exact function before storage. A crash here would be a remotely + // triggerable DoS against an authenticated user's own session, so + // "never panics" matters more here than for the calendar/contact + // parsers. + defer func() { + if r := recover(); r != nil { + t.Fatalf("Parse panicked on input %q: %v", data, r) + } + }() + Parse(data) + }) +} diff --git a/internal/smtp/auth.go b/internal/smtp/auth.go new file mode 100644 index 0000000..80be145 --- /dev/null +++ b/internal/smtp/auth.go @@ -0,0 +1,11 @@ +package smtp + +import ( + "gomail/internal/auth" + "gomail/internal/db" +) + +// authenticate is a thin wrapper over the shared auth package, scoped to SMTP. +func authenticate(database *db.DB, username, password string) (*db.User, bool) { + return auth.Authenticate(database, username, password, auth.ScopeSMTP) +} diff --git a/internal/smtp/server.go b/internal/smtp/server.go new file mode 100644 index 0000000..c77d772 --- /dev/null +++ b/internal/smtp/server.go @@ -0,0 +1,212 @@ +// Package smtp implements the inbound SMTP MTA (port 25), submission +// (port 587, STARTTLS + AUTH), and implicit-TLS SMTPS (port 465) — all as a +// single hand-rolled state machine over net.Listener, per the project's +// stdlib-first principle. No third-party SMTP library. +package smtp + +import ( + "context" + "crypto/tls" + "fmt" + "log/slog" + "net" + "sync" + "time" + + "gomail/internal/config" + "gomail/internal/db" + "gomail/internal/mailstore" + "gomail/internal/pipeline" + "gomail/internal/ratelimit" +) + +const ( + maxCommandLine = 1000 // RFC 5321 command line limit + maxRecipients = 100 + idleTimeout = 5 * time.Minute + dataTimeout = 10 * time.Minute +) + +// Kind distinguishes the three listener roles — they share the same session +// state machine but differ in whether TLS is implicit, STARTTLS-capable, or +// plain (inbound MTA still offers STARTTLS, just doesn't require it for the +// initial MAIL FROM the way submission does). +type Kind int + +const ( + KindMTA Kind = iota // :25 — inbound from the internet, STARTTLS optional + KindSubmission // :587 — STARTTLS + AUTH required before MAIL FROM + KindImplicitTLS // :465 — TLS from the first byte +) + +type Server struct { + cfg *config.Config + database *db.DB + store *mailstore.Store + tlsConf *tls.Config + pipeline *pipeline.Orchestrator // nil = pipeline disabled, all mail treated as clean + + listeners []net.Listener + wg sync.WaitGroup + sessionWG sync.WaitGroup // tracks in-flight sessions for graceful drain + + maxMessageBytes int64 + + connLimiter *ratelimit.Limiter // per-IP connections/min, cfg.RateLimits.SMTPConnPerMin + authLimiter *ratelimit.Limiter // per-IP AUTH failures, cfg.RateLimits.SMTPAuthFailures (per minute) +} + +func NewServer(cfg *config.Config, database *db.DB, store *mailstore.Store, tlsConf *tls.Config, orch *pipeline.Orchestrator) *Server { + return &Server{ + cfg: cfg, + database: database, + store: store, + tlsConf: tlsConf, + pipeline: orch, + maxMessageBytes: int64(cfg.Storage.MaxMessageSizeMB) * 1024 * 1024, + connLimiter: ratelimit.New(cfg.RateLimits.SMTPConnPerMin), + authLimiter: ratelimit.New(cfg.RateLimits.SMTPAuthFailures), + } +} + +// ListenAndServe starts all three listeners and blocks until one fails or +// ctx is cancelled. Each listener's accept loop runs in its own goroutine. +func (s *Server) ListenAndServe(ctx context.Context) error { + specs := []struct { + addr string + kind Kind + }{ + {s.cfg.Server.SMTPAddr, KindMTA}, + {s.cfg.Server.SubmissionAddr, KindSubmission}, + {s.cfg.Server.SMTPSAddr, KindImplicitTLS}, + } + + errCh := make(chan error, len(specs)) + + for _, spec := range specs { + ln, err := net.Listen("tcp", spec.addr) + if err != nil { + s.closeAll() + return fmt.Errorf("listen %s: %w", spec.addr, err) + } + + if spec.kind == KindImplicitTLS { + ln = tls.NewListener(ln, s.tlsConf) + } + + s.listeners = append(s.listeners, ln) + slog.Info("SMTP listener started", "addr", spec.addr, "kind", kindName(spec.kind)) + + s.wg.Add(1) + go func(ln net.Listener, kind Kind) { + defer s.wg.Done() + s.acceptLoop(ctx, ln, kind) + }(ln, spec.kind) + } + + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errCh: + return err + } +} + +func (s *Server) acceptLoop(ctx context.Context, ln net.Listener, kind Kind) { + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-ctx.Done(): + return // expected — listener closed during shutdown + default: + slog.Error("accept error", "err", err, "kind", kindName(kind)) + return + } + } + + ip := connHost(conn.RemoteAddr()) + if !s.connLimiter.Allow(ip) { + slog.Warn("SMTP connection rate limit exceeded, rejecting", "ip", ip, "kind", kindName(kind)) + conn.Close() + continue + } + + s.sessionWG.Add(1) + go func() { + defer s.sessionWG.Done() + s.handleConn(ctx, conn, kind) + }() + } +} + +func (s *Server) handleConn(ctx context.Context, conn net.Conn, kind Kind) { + defer conn.Close() + + sess := &session{ + conn: conn, + server: s, + kind: kind, + hostname: s.cfg.Server.Hostname, + } + + remoteAddr := conn.RemoteAddr() + if tcpAddr, ok := remoteAddr.(*net.TCPAddr); ok { + sess.senderIP = tcpAddr.IP + } + + slog.Debug("SMTP connection accepted", "remote", remoteAddr, "kind", kindName(kind)) + sess.run(ctx) +} + +// Shutdown closes all listeners immediately (stops accepting new +// connections) then waits up to gracePeriod for in-flight sessions to finish +// naturally (they'll see ctx.Done() and wind down at their next command read). +func (s *Server) Shutdown(gracePeriod time.Duration) { + s.closeAll() + + done := make(chan struct{}) + go func() { + s.sessionWG.Wait() + close(done) + }() + + select { + case <-done: + slog.Info("all SMTP sessions drained cleanly") + case <-time.After(gracePeriod): + slog.Warn("SMTP shutdown grace period expired — some sessions forcibly terminated", "grace_period", gracePeriod) + } +} + +func (s *Server) closeAll() { + for _, ln := range s.listeners { + ln.Close() + } + s.wg.Wait() +} + +func kindName(k Kind) string { + switch k { + case KindMTA: + return "mta" + case KindSubmission: + return "submission" + case KindImplicitTLS: + return "smtps" + default: + return "unknown" + } +} + +// connHost extracts just the IP (no port) from a net.Addr, for use as a +// rate-limiter key — falls back to the full address string if it isn't +// host:port shaped (shouldn't happen for real TCP connections, but a +// fallback beats a panic). +func connHost(addr net.Addr) string { + host, _, err := net.SplitHostPort(addr.String()) + if err != nil { + return addr.String() + } + return host +} diff --git a/internal/smtp/session.go b/internal/smtp/session.go new file mode 100644 index 0000000..8d71761 --- /dev/null +++ b/internal/smtp/session.go @@ -0,0 +1,762 @@ +package smtp + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "log/slog" + "net" + "net/mail" + "strings" + "time" + + "gomail/internal/db" + "gomail/internal/pipeline" + "gomail/internal/sieve" + "github.com/google/uuid" +) + +type state int + +const ( + stateGreeted state = iota + stateAuthenticated + stateMailFrom + stateRcptTo +) + +type session struct { + conn net.Conn + rw *bufio.ReadWriter + server *Server + kind Kind + hostname string + senderIP net.IP + senderHost string + + state state + tlsActive bool + authUser *db.User + mailFrom string + rcptTo []string + recipientsValid []recipientTarget +} + +type recipientTarget struct { + address string + user *db.User // nil if only validated as accept-all domain (no specific mailbox yet resolvable) + tenantID string +} + +func (s *session) isSubmissionKind() bool { + return s.kind == KindSubmission || s.kind == KindImplicitTLS +} + +func (s *session) run(ctx context.Context) { + s.rw = bufio.NewReadWriter(bufio.NewReader(s.conn), bufio.NewWriter(s.conn)) + + if s.kind == KindImplicitTLS { + s.tlsActive = true // listener already wrapped with tls.NewListener + } + + s.writeLine(fmt.Sprintf("220 %s GoMail ESMTP ready", s.hostname)) + + for { + select { + case <-ctx.Done(): + s.writeLine("421 4.3.2 Server shutting down") + return + default: + } + + s.conn.SetReadDeadline(time.Now().Add(idleTimeout)) + line, err := s.readLine() + if err != nil { + if err != io.EOF { + slog.Debug("SMTP read error", "err", err) + } + return + } + + if !s.handleCommand(ctx, line) { + return // QUIT or fatal error + } + } +} + +// handleCommand dispatches one command line. Returns false if the session +// should close (QUIT or unrecoverable error). +func (s *session) handleCommand(ctx context.Context, line string) bool { + if len(line) > maxCommandLine { + s.writeLine("500 5.5.2 Line too long") + return true + } + + verb, rest := splitVerb(line) + + switch strings.ToUpper(verb) { + case "HELO": + s.handleHelo(rest, false) + case "EHLO": + s.handleHelo(rest, true) + case "STARTTLS": + s.handleStartTLS() + case "AUTH": + s.handleAuth(rest) + case "MAIL": + s.handleMailFrom(rest) + case "RCPT": + s.handleRcptTo(rest) + case "DATA": + s.handleData(ctx) + case "RSET": + s.reset() + s.writeLine("250 2.0.0 OK") + case "NOOP": + s.writeLine("250 2.0.0 OK") + case "QUIT": + s.writeLine(fmt.Sprintf("221 2.0.0 %s closing connection", s.hostname)) + return false + case "VRFY", "EXPN": + // Information disclosure — always decline, never confirm/deny addresses. + s.writeLine("252 2.5.2 Cannot VRFY user, but will accept message and attempt delivery") + default: + s.writeLine("500 5.5.1 Command not recognized") + } + return true +} + +func (s *session) handleHelo(arg string, extended bool) { + if arg == "" { + s.writeLine("501 5.5.4 HELO/EHLO requires a hostname argument") + return + } + s.reset() + s.state = stateGreeted + + if !extended { + s.writeLine(fmt.Sprintf("250 %s", s.hostname)) + return + } + + caps := []string{ + fmt.Sprintf("250-%s", s.hostname), + "250-PIPELINING", + fmt.Sprintf("250-SIZE %d", s.server.maxMessageBytes), + "250-8BITMIME", + } + if !s.tlsActive { + caps = append(caps, "250-STARTTLS") + } + if s.isSubmissionKind() && s.tlsActive { + caps = append(caps, "250-AUTH PLAIN LOGIN") + } + caps = append(caps, "250 ENHANCEDSTATUSCODES") + + for _, c := range caps { + s.writeLine(c) + } +} + +func (s *session) handleStartTLS() { + if s.tlsActive { + s.writeLine("503 5.5.1 TLS already active") + return + } + s.writeLine("220 2.0.0 Ready to start TLS") + + tlsConn := tls.Server(s.conn, s.server.tlsConf) + if err := tlsConn.HandshakeContext(context.Background()); err != nil { + slog.Debug("STARTTLS handshake failed", "err", err) + return + } + + s.conn = tlsConn + s.rw = bufio.NewReadWriter(bufio.NewReader(tlsConn), bufio.NewWriter(tlsConn)) + s.tlsActive = true + s.reset() // RFC 3207 — discard any prior state after STARTTLS + s.state = stateGreeted +} + +// handleAuth implements SASL PLAIN and LOGIN. Verifies against either the +// user's main password (bcrypt) or an active, non-expired app password +// scoped for "smtp". Submission (:587) requires TLS to be active first. +func (s *session) handleAuth(arg string) { + if s.kind != KindSubmission && s.kind != KindImplicitTLS { + s.writeLine("503 5.5.1 AUTH not permitted on this port") + return + } + if !s.tlsActive { + s.writeLine("538 5.7.11 Encryption required for requested authentication mechanism") + return + } + + // Checked before attempting any credential parsing — an IP that has + // already exhausted its allowance shouldn't get free password-guessing + // attempts just because the failure hasn't been recorded yet. + ip := connHost(s.conn.RemoteAddr()) + if !s.server.authLimiter.Allow(ip) { + slog.Warn("SMTP AUTH rate limit exceeded", "remote", ip) + s.writeLine("454 4.7.0 Too many authentication attempts, try again later") + return + } + + mechanism, initialResponse, _ := strings.Cut(arg, " ") + mechanism = strings.ToUpper(mechanism) + + var username, password string + var ok bool + + switch mechanism { + case "PLAIN": + username, password, ok = s.readAuthPlain(initialResponse) + case "LOGIN": + username, password, ok = s.readAuthLogin() + default: + s.writeLine("504 5.5.4 Unrecognized authentication mechanism") + return + } + if !ok { + s.writeLine("501 5.5.4 Malformed authentication response") + return + } + + user, verified := authenticate(s.server.database, username, password) + if !verified { + slog.Info("SMTP auth failed", "user", username, "remote", s.senderIP) + s.writeLine("535 5.7.8 Authentication credentials invalid") + return + } + + s.authUser = user + s.state = stateAuthenticated + s.writeLine("235 2.7.0 Authentication successful") +} + +func (s *session) readAuthPlain(initial string) (username, password string, ok bool) { + raw := initial + if raw == "" { + s.writeLine("334 ") + line, err := s.readLine() + if err != nil { + return "", "", false + } + raw = line + } + decoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return "", "", false + } + // SASL PLAIN format: authzid\0authcid\0password + parts := strings.SplitN(string(decoded), "\x00", 3) + if len(parts) != 3 { + return "", "", false + } + return parts[1], parts[2], true +} + +func (s *session) readAuthLogin() (username, password string, ok bool) { + s.writeLine("334 VXNlcm5hbWU6") // "Username:" + uLine, err := s.readLine() + if err != nil { + return "", "", false + } + uDecoded, err := base64.StdEncoding.DecodeString(uLine) + if err != nil { + return "", "", false + } + + s.writeLine("334 UGFzc3dvcmQ6") // "Password:" + pLine, err := s.readLine() + if err != nil { + return "", "", false + } + pDecoded, err := base64.StdEncoding.DecodeString(pLine) + if err != nil { + return "", "", false + } + + return string(uDecoded), string(pDecoded), true +} + +func (s *session) handleMailFrom(arg string) { + if s.isSubmissionKind() && s.state != stateAuthenticated { + s.writeLine("530 5.7.0 Authentication required") + return + } + + addr, ok := parseMailCmdArg(arg, "FROM:") + if !ok { + s.writeLine("501 5.5.4 Syntax error in MAIL FROM command") + return + } + + // Submission: envelope sender must match the authenticated user (or their alias). + if s.isSubmissionKind() && addr != "" { + if !strings.EqualFold(addr, s.authUser.Email) { + s.writeLine("553 5.7.1 MAIL FROM must match authenticated identity") + return + } + } + + s.mailFrom = strings.ToLower(addr) + s.rcptTo = nil + s.recipientsValid = nil + s.state = stateMailFrom + s.writeLine("250 2.1.0 OK") +} + +func (s *session) handleRcptTo(arg string) { + if s.state != stateMailFrom && s.state != stateRcptTo { + s.writeLine("503 5.5.1 MAIL FROM required before RCPT TO") + return + } + if len(s.rcptTo) >= maxRecipients { + s.writeLine("452 4.5.3 Too many recipients") + return + } + + addr, ok := parseMailCmdArg(arg, "TO:") + if !ok || addr == "" { + s.writeLine("501 5.5.4 Syntax error in RCPT TO command") + return + } + addr = strings.ToLower(addr) + + parts := strings.SplitN(addr, "@", 2) + if len(parts) != 2 { + s.writeLine("501 5.1.3 Bad recipient address syntax") + return + } + domainPart := parts[1] + + // Outbound relay (submission, authenticated) — recipient is external, no local check. + if s.isSubmissionKind() && s.authUser != nil { + s.rcptTo = append(s.rcptTo, addr) + s.recipientsValid = append(s.recipientsValid, recipientTarget{address: addr, tenantID: s.authUser.TenantID}) + s.state = stateRcptTo + s.writeLine("250 2.1.5 OK") + return + } + + // Inbound — recipient must be a hosted domain, and either accept-all or a known user. + domain, tenant, err := s.server.database.LookupDomain(domainPart) + if err != nil { + slog.Debug("RCPT rejected — unknown domain", "domain", domainPart) + s.writeLine("550 5.1.2 Bad destination mailbox address") + return + } + + // Sender IP/address block-list check. + senderDomain := "" + if i := strings.LastIndex(s.mailFrom, "@"); i >= 0 { + senderDomain = s.mailFrom[i+1:] + } + if blocked, action, _ := s.server.database.MatchListRule(tenant.ID, s.mailFrom, senderDomain); blocked && action == db.ListActionBlock { + slog.Info("RCPT rejected — sender blocked by list rule", "from", s.mailFrom, "to", addr) + s.writeLine("550 5.7.1 Sender rejected") + return + } + + var user *db.User + if u, err := s.server.database.LookupUserByEmail(addr); err == nil { + user = u + } else if !domain.AcceptAll { + slog.Debug("RCPT rejected — unknown user, domain not accept-all", "to", addr) + s.writeLine("550 5.1.1 User unknown") + return + } + + s.rcptTo = append(s.rcptTo, addr) + s.recipientsValid = append(s.recipientsValid, recipientTarget{address: addr, user: user, tenantID: tenant.ID}) + s.state = stateRcptTo + s.writeLine("250 2.1.5 OK") +} + +func (s *session) handleData(ctx context.Context) { + if s.state != stateRcptTo || len(s.rcptTo) == 0 { + s.writeLine("503 5.5.1 RCPT TO required before DATA") + return + } + + s.writeLine("354 Start mail input; end with .") + s.conn.SetReadDeadline(time.Now().Add(dataTimeout)) + + raw, err := s.readDotStuffed() + if err != nil { + s.writeLine("451 4.3.0 Error reading message data") + return + } + if int64(len(raw)) > s.server.maxMessageBytes { + s.writeLine(fmt.Sprintf("552 5.3.4 Message size exceeds maximum of %d bytes", s.server.maxMessageBytes)) + s.reset() + return + } + + subject := extractSubject(raw) + msgIDHdr := extractMessageID(raw) + + deliveredCount := 0 + for _, target := range s.recipientsValid { + msgID := uuid.NewString() + + msg := &db.Message{ + ID: msgID, + TenantID: target.tenantID, + FromAddress: s.mailFrom, + ToAddress: target.address, + Subject: subject, + MessageIDHdr: msgIDHdr, + SizeBytes: int64(len(raw)), + Verdict: db.VerdictClean, + SenderIP: senderIPString(s.senderIP), + } + + // Insert the audit row immediately — message_checks rows inserted by + // the pipeline below FK-reference messages.id, so the parent row + // must exist first regardless of how long pipeline evaluation takes. + if err := s.server.database.InsertMessage(msg); err != nil { + slog.Error("failed to record message audit row", "err", err) + } + + if target.user != nil { + deliverRaw := raw + msg.Verdict = db.VerdictClean + + // Run the security pipeline only for true inbound mail from the + // internet (KindMTA) — mail submitted by an authenticated local + // user to another local user (KindSubmission/KindImplicitTLS) is + // treated as trusted internal mail and skips filtering, matching + // standard MTA practice. + if s.kind == KindMTA && s.server.pipeline != nil { + mc := &pipeline.MailContext{ + SenderIP: s.senderIP, + SenderHost: s.senderHost, + MailFrom: s.mailFrom, + RcptTo: target.address, + RawMessage: raw, + } + s.server.pipeline.Run(ctx, mc) + msg.Verdict = mc.Verdict + msg.TotalScore = mc.TotalScore + + for _, check := range mc.Checks { + mcRow := &db.MessageCheck{ + ID: uuid.NewString(), + MessageID: msgID, + Stage: check.Stage, + Result: check.Result, + Score: check.Score, + Detail: check.Detail, + DurationMs: check.DurationMs, + } + if err := s.server.database.InsertMessageCheck(mcRow); err != nil { + slog.Error("failed to record pipeline check result", "err", err) + } + } + + if msg.Verdict == db.VerdictFlagged { + deliverRaw = injectSpamHeaders(raw, mc.TotalScore, mc.Checks) + } + } + + switch msg.Verdict { + case db.VerdictQuarantine, db.VerdictBlocked: + if err := s.quarantineMessage(msgID, raw, msg.Verdict); err != nil { + slog.Error("quarantine failed", "to", target.address, "err", err) + continue + } + slog.Info("message quarantined", "to", target.address, "verdict", msg.Verdict, "score", msg.TotalScore) + if err := s.server.database.UpdateMessageVerdict(msgID, msg.Verdict, msg.TotalScore, nil); err != nil { + slog.Error("failed to update message verdict", "err", err) + } + deliveredCount++ // "accepted" from the SMTP client's perspective — held, not bounced + default: + // Clean or flagged — check for an active Sieve script before + // delivering, so fileinto/discard rules apply to the same + // mail the security pipeline already cleared. + destFolder := "INBOX" + discard := false + if script, err := s.server.database.GetActiveSieveScript(target.user.ID); err == nil { + if result, applyErr := applySieve(script.ScriptText, deliverRaw); applyErr == nil { + switch result.Action { + case "fileinto": + destFolder = result.Folder + case "discard": + discard = true + } + } else { + slog.Warn("sieve script failed to apply, falling back to INBOX delivery", "user", target.user.Email, "err", applyErr) + } + } + + now := time.Now().UTC() + if discard { + slog.Info("message discarded by sieve rule", "to", target.address) + if err := s.server.database.UpdateMessageVerdict(msgID, msg.Verdict, msg.TotalScore, &now); err != nil { + slog.Error("failed to update message verdict", "err", err) + } + deliveredCount++ // accepted from the SMTP client's perspective, then discarded per user's own rule + continue + } + + if _, err := s.server.store.Deliver(target.user.ID, target.user.Email, destFolder, deliverRaw); err != nil { + slog.Error("local delivery failed", "to", target.address, "folder", destFolder, "err", err) + continue + } + msg.RelayedAt = &now + if err := s.server.database.UpdateMessageVerdict(msgID, msg.Verdict, msg.TotalScore, &now); err != nil { + slog.Error("failed to update message verdict", "err", err) + } + deliveredCount++ + } + } else if s.isSubmissionKind() { + // Outbound to external address — stage the message and enqueue it + // for the background queue worker (internal/queue) to deliver. + _, queuePath, err := s.server.store.WriteQueueFile(raw) + if err != nil { + slog.Error("failed to stage outbound message", "to", target.address, "err", err) + continue + } + qEntry := &db.OutboundQueueEntry{ + ID: uuid.NewString(), + UserID: s.authUser.ID, + FromAddress: s.mailFrom, + ToAddress: target.address, + EMLPath: queuePath, + NextAttemptAt: time.Now().UTC(), + } + if err := s.server.database.InsertOutboundQueueEntry(qEntry); err != nil { + slog.Error("failed to enqueue outbound message", "to", target.address, "err", err) + continue + } + now := time.Now().UTC() + s.server.database.UpdateMessageVerdict(msgID, db.VerdictClean, 0, &now) + slog.Info("outbound message queued", "to", target.address, "from", s.mailFrom) + deliveredCount++ + } else { + slog.Warn("accept-all domain recipient has no mailbox yet — message accepted but not delivered", "to", target.address) + } + } + + if deliveredCount == 0 { + s.writeLine("451 4.3.0 Temporary delivery failure") + s.reset() + return + } + + s.writeLine("250 2.0.0 OK: message accepted") + s.reset() +} + +// quarantineMessage stores the raw message encrypted in the quarantine area +// and creates the DB entry — called when the pipeline verdict is quarantine +// or blocked. The message is NOT delivered to the recipient's mailbox; it's +// held for admin/user review (release flow lands with the webmail/admin +// portal in a later phase; for now this establishes the storage half). +func (s *session) quarantineMessage(msgID string, raw []byte, verdict db.MessageVerdict) error { + path, err := s.server.store.WriteQuarantineFile(msgID, raw) + if err != nil { + return fmt.Errorf("write quarantine file: %w", err) + } + + entry := &db.QuarantineEntry{ + ID: uuid.NewString(), + MessageID: msgID, + EMLPath: path, + Status: db.QuarantineHeld, + Reason: fmt.Sprintf("verdict=%s", verdict), + ExpiresAt: time.Now().UTC().AddDate(0, 0, s.server.cfg.Storage.QuarantineDays), + } + if err := s.server.database.InsertQuarantineEntry(entry); err != nil { + return fmt.Errorf("insert quarantine entry: %w", err) + } + return nil +} + +// injectSpamHeaders prepends X-Spam-* headers to a flagged (but still +// delivered) message so the recipient's mail client / webmail can surface +// the pipeline's findings without the message needing to be held. +// applySieve parses and executes a user's active Sieve script against a +// message's headers, returning the routing decision (fileinto/discard/keep). +// Headers are extracted fresh from raw rather than reusing any previously +// parsed structure, since this runs after the pipeline may have prepended +// X-Spam-* headers (injectSpamHeaders) — the script should see exactly what +// will be delivered, filters included. +func applySieve(scriptText string, raw []byte) (sieve.Result, error) { + parsed, err := sieve.Parse(scriptText) + if err != nil { + return sieve.Result{}, fmt.Errorf("parse: %w", err) + } + headers := extractHeaderMap(raw) + return sieve.Execute(parsed, headers), nil +} + +// extractHeaderMap does a lightweight single-value-per-header extraction +// (last value wins for repeated headers) — sufficient for the header +// :contains / :is tests this Sieve subset supports. +func extractHeaderMap(raw []byte) map[string]string { + headers := map[string]string{} + text := string(raw) + headerEnd := strings.Index(text, "\r\n\r\n") + if headerEnd == -1 { + headerEnd = len(text) + } + for _, line := range strings.Split(text[:headerEnd], "\r\n") { + if line == "" { + continue + } + if (strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")) && len(headers) > 0 { + continue // folded continuation — good enough for this subset, not appended + } + name, value, found := strings.Cut(line, ":") + if !found { + continue + } + headers[strings.TrimSpace(name)] = strings.TrimSpace(value) + } + return headers +} + +func injectSpamHeaders(raw []byte, score float64, checks []pipeline.StageResult) []byte { + var failedStages []string + for _, c := range checks { + if c.Result == db.CheckFail || c.Result == db.CheckWarn { + failedStages = append(failedStages, c.Stage) + } + } + + header := fmt.Sprintf("X-Spam-Score: %.1f\r\nX-Spam-Flag: YES\r\n", score) + if len(failedStages) > 0 { + header += fmt.Sprintf("X-Spam-Checks: %s\r\n", strings.Join(failedStages, ", ")) + } + return append([]byte(header), raw...) +} + +func (s *session) reset() { + s.mailFrom = "" + s.rcptTo = nil + s.recipientsValid = nil + if s.state != stateAuthenticated { + s.state = stateGreeted + } else { + s.state = stateAuthenticated + } +} + +// ── I/O helpers ───────────────────────────────────────────────────────────────── + +func (s *session) writeLine(line string) { + s.rw.WriteString(line) + s.rw.WriteString("\r\n") + s.rw.Flush() +} + +func (s *session) readLine() (string, error) { + line, err := s.rw.ReadString('\n') + if err != nil { + return "", err + } + return strings.TrimRight(line, "\r\n"), nil +} + +// readDotStuffed reads the DATA payload until the terminating "\r\n.\r\n", +// undoing dot-stuffing (a line starting with ".." becomes ".") per RFC 5321 §4.5.2. +func (s *session) readDotStuffed() ([]byte, error) { + var buf []byte + for { + line, err := s.rw.ReadString('\n') + if err != nil { + return nil, err + } + trimmed := strings.TrimRight(line, "\r\n") + if trimmed == "." { + return buf, nil + } + if strings.HasPrefix(trimmed, "..") { + trimmed = trimmed[1:] + } + buf = append(buf, []byte(trimmed)...) + buf = append(buf, '\r', '\n') + + if int64(len(buf)) > s.server.maxMessageBytes+1024 { + return nil, fmt.Errorf("message exceeds max size during read") + } + } +} + +// ── Parsing helpers ─────────────────────────────────────────────────────────── + +func splitVerb(line string) (verb, rest string) { + line = strings.TrimSpace(line) + i := strings.IndexAny(line, " :") + if i < 0 { + return line, "" + } + // Keep MAIL FROM: / RCPT TO: colon attached to rest for parseMailCmdArg. + if line[i] == ':' { + return line[:i], line[i:] + } + return line[:i], strings.TrimSpace(line[i+1:]) +} + +// parseMailCmdArg extracts the address from "FROM:" or "TO:" — +// tolerant of the colon being split into verb or rest depending on spacing. +func parseMailCmdArg(arg, prefix string) (string, bool) { + arg = strings.TrimSpace(arg) + upper := strings.ToUpper(arg) + prefixUpper := strings.ToUpper(prefix) + if strings.HasPrefix(upper, prefixUpper) { + arg = arg[len(prefix):] + } else if strings.HasPrefix(upper, ":") { + arg = arg[1:] + } + arg = strings.TrimSpace(arg) + + // Strip angle brackets and any trailing ESMTP parameters (e.g. "SIZE=1234"). + if i := strings.Index(arg, ">"); i >= 0 { + arg = arg[:i+1] + } + arg = strings.TrimPrefix(arg, "<") + arg = strings.TrimSuffix(arg, ">") + arg = strings.TrimSpace(arg) + + if arg == "" { + return "", true // null sender (bounces) is valid: MAIL FROM:<> + } + if _, err := mail.ParseAddress(arg); err != nil { + return "", false + } + return arg, true +} + +func senderIPString(ip net.IP) string { + if ip == nil { + return "" + } + return ip.String() +} + +func extractSubject(raw []byte) string { + return extractHeader(raw, "Subject:") +} + +func extractMessageID(raw []byte) string { + return extractHeader(raw, "Message-Id:") +} + +func extractHeader(raw []byte, prefix string) string { + lines := strings.Split(string(raw), "\r\n") + for _, line := range lines { + if line == "" { + break // end of headers + } + if strings.HasPrefix(strings.ToLower(line), strings.ToLower(prefix)) { + return strings.TrimSpace(line[len(prefix):]) + } + } + return "" +} diff --git a/internal/tlsutil/acme_manager.go b/internal/tlsutil/acme_manager.go new file mode 100644 index 0000000..439b91a --- /dev/null +++ b/internal/tlsutil/acme_manager.go @@ -0,0 +1,227 @@ +package tlsutil + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "log/slog" + "sync" + "time" + + "gomail/internal/acme" + "gomail/internal/crypto" + "gomail/internal/db" +) + +// renewalMargin is how far before expiry a certificate is renewed. +const renewalMargin = 30 * 24 * time.Hour + +// ACMEManager obtains and caches ACME certificates per domain, encrypted at +// rest (same HKDF-per-record scheme as everything else), and serves them +// via a SNI-aware tls.Config.GetCertificate callback so a single listener +// can present the right certificate for whichever domain a client connects +// to. A background loop renews any certificate within renewalMargin of +// expiry. +type ACMEManager struct { + database *db.DB + mk *crypto.MasterKey + directoryURL string + contactEmail string + responder *acme.ChallengeResponder + + mu sync.RWMutex + cache map[string]*tls.Certificate +} + +func NewACMEManager(database *db.DB, mk *crypto.MasterKey, directoryURL, contactEmail string, responder *acme.ChallengeResponder) *ACMEManager { + return &ACMEManager{ + database: database, mk: mk, directoryURL: directoryURL, contactEmail: contactEmail, + responder: responder, cache: make(map[string]*tls.Certificate), + } +} + +// TLSConfig returns a tls.Config whose GetCertificate looks up the right +// cert per SNI, obtaining one on first use if none is cached yet. +func (m *ACMEManager) TLSConfig() *tls.Config { + return &tls.Config{ + MinVersion: tls.VersionTLS12, + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return m.CertificateFor(hello.ServerName) + }, + } +} + +// CertificateFor returns a cached certificate for domain, obtaining one via +// ACME (and caching it, in memory and encrypted in the DB) if not already +// cached or if the cached one is expired/near expiry. +func (m *ACMEManager) CertificateFor(domain string) (*tls.Certificate, error) { + m.mu.RLock() + cached, ok := m.cache[domain] + m.mu.RUnlock() + if ok { + return cached, nil + } + + if stored, err := m.loadFromDB(domain); err == nil { + m.mu.Lock() + m.cache[domain] = stored + m.mu.Unlock() + return stored, nil + } + + cert, err := m.obtainAndStore(domain) + if err != nil { + return nil, err + } + return cert, nil +} + +func (m *ACMEManager) loadFromDB(domain string) (*tls.Certificate, error) { + row, err := m.database.GetTLSCert(domain) + if err != nil { + return nil, err + } + if row.CertPEMEnc == nil || row.KeyPEMEnc == nil { + return nil, fmt.Errorf("no cert material stored for %s", domain) + } + if row.ExpiresAt != nil && time.Now().UTC().After(row.ExpiresAt.Add(-renewalMargin)) { + return nil, fmt.Errorf("stored cert for %s is expired or near expiry", domain) + } + + certPEM, err := crypto.Decrypt(m.mk, row.ID, "tls-cert", row.CertPEMEnc) + if err != nil { + return nil, fmt.Errorf("decrypting cert: %w", err) + } + keyPEM, err := crypto.Decrypt(m.mk, row.ID, "tls-key", row.KeyPEMEnc) + if err != nil { + return nil, fmt.Errorf("decrypting key: %w", err) + } + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return nil, fmt.Errorf("parsing stored cert/key: %w", err) + } + return &cert, nil +} + +func (m *ACMEManager) obtainAndStore(domain string) (*tls.Certificate, error) { + accountKey, err := m.loadOrCreateAccountKey(domain) + if err != nil { + return nil, fmt.Errorf("account key: %w", err) + } + + slog.Info("obtaining ACME certificate", "domain", domain, "directory", m.directoryURL) + certPEM, keyPEM, err := acme.Obtain(m.directoryURL, m.contactEmail, []string{domain}, accountKey, m.responder) + if err != nil { + return nil, fmt.Errorf("ACME obtain for %s: %w", domain, err) + } + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return nil, fmt.Errorf("parsing obtained cert/key: %w", err) + } + + var expiresAt *time.Time + if len(cert.Certificate) > 0 { + if leaf, err := x509.ParseCertificate(cert.Certificate[0]); err == nil { + expiresAt = &leaf.NotAfter + } + } + + existing, _ := m.database.GetTLSCert(domain) + recordID := domain + if existing != nil { + recordID = existing.ID + } + encCert, err := crypto.Encrypt(m.mk, recordID, "tls-cert", certPEM) + if err != nil { + return nil, fmt.Errorf("encrypting cert: %w", err) + } + encKey, err := crypto.Encrypt(m.mk, recordID, "tls-key", keyPEM) + if err != nil { + return nil, fmt.Errorf("encrypting key: %w", err) + } + + if err := m.database.UpsertTLSCert(&db.TLSCert{ + ID: recordID, Domain: domain, CertPEMEnc: encCert, KeyPEMEnc: encKey, ExpiresAt: expiresAt, + }); err != nil { + return nil, fmt.Errorf("storing cert: %w", err) + } + + m.mu.Lock() + m.cache[domain] = &cert + m.mu.Unlock() + + slog.Info("ACME certificate obtained and stored", "domain", domain, "expires_at", expiresAt) + return &cert, nil +} + +func (m *ACMEManager) loadOrCreateAccountKey(domain string) (*acme.AccountKey, error) { + row, err := m.database.GetTLSCert(domain) + if err == nil && row.ACMEAccountKeyEnc != nil { + plain, decErr := crypto.Decrypt(m.mk, row.ID, "acme-account-key", row.ACMEAccountKeyEnc) + if decErr == nil { + if key, parseErr := acme.ParseAccountKeyPEM(plain); parseErr == nil { + return key, nil + } + } + } + + key, err := acme.GenerateAccountKey() + if err != nil { + return nil, err + } + keyPEM, err := key.MarshalPEM() + if err != nil { + return nil, err + } + + recordID := domain + if row != nil { + recordID = row.ID + } + encKey, err := crypto.Encrypt(m.mk, recordID, "acme-account-key", keyPEM) + if err != nil { + return nil, err + } + if err := m.database.SetACMEAccountKey(domain, encKey); err != nil { + return nil, err + } + return key, nil +} + +// StartRenewalLoop runs a background check (default: daily) and renews any +// domain whose cached/stored certificate is within renewalMargin of expiry. +// domains is the full set this instance is responsible for — typically all +// active hosted domains plus the server's own hostname. +func (m *ACMEManager) StartRenewalLoop(ctx context.Context, domains []string, checkInterval time.Duration) { + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + checkAndRenew := func() { + for _, domain := range domains { + row, err := m.database.GetTLSCert(domain) + needsRenewal := err != nil || row.ExpiresAt == nil || time.Now().UTC().After(row.ExpiresAt.Add(-renewalMargin)) + if !needsRenewal { + continue + } + slog.Info("renewing ACME certificate", "domain", domain) + m.mu.Lock() + delete(m.cache, domain) // force re-obtain, not a stale in-memory hit + m.mu.Unlock() + if _, err := m.obtainAndStore(domain); err != nil { + slog.Error("ACME renewal failed", "domain", domain, "err", err) + } + } + } + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + checkAndRenew() + } + } +} diff --git a/internal/tlsutil/selfsigned.go b/internal/tlsutil/selfsigned.go new file mode 100644 index 0000000..30b9ca7 --- /dev/null +++ b/internal/tlsutil/selfsigned.go @@ -0,0 +1,114 @@ +// Package tlsutil provides certificate loading: LoadOrGenerate for the +// file/self-signed paths (this file), and ACMEManager (acme_manager.go) for +// real Let's Encrypt-style issuance via internal/acme. The self-signed +// generator here remains the fallback for tls.mode "off" or "file" without +// a cert on disk yet — genuinely necessary for local dev/testing, not a +// placeholder for a missing feature. +package tlsutil + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "log/slog" + "math/big" + "net" + "time" +) + +// LoadOrGenerate returns a tls.Config for the given mode: +// - "file": load cert/key from disk paths +// - anything else ("acme" not yet implemented, "off"): generate a self-signed +// cert so STARTTLS/IMAPS/etc. still work during development. Logs a loud +// warning since this is never appropriate for production. +func LoadOrGenerate(mode, hostname, certFile, keyFile string, minVersion uint16) (*tls.Config, error) { + var cert tls.Certificate + var err error + + switch mode { + case "file": + cert, err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("loading TLS cert/key: %w", err) + } + case "acme": + // Reaching here (rather than the real ACMEManager path in main.go) + // means mode=="acme" but no acme_domains were configured — a real + // ACME client exists (internal/acme, wired in main.go), it's just + // not usable without knowing which domain(s) to request a cert for. + slog.Warn("TLS mode is 'acme' but no acme_domains are configured — "+ + "generating a SELF-SIGNED certificate instead. Set tls.acme_domains "+ + "in config.yaml to enable real Let's Encrypt issuance.", + "hostname", hostname) + cert, err = generateSelfSigned(hostname) + if err != nil { + return nil, fmt.Errorf("generating self-signed cert: %w", err) + } + default: + slog.Warn("TLS mode is 'off' — generating a SELF-SIGNED certificate. "+ + "This is fine for local testing but MUST NOT be used in production; "+ + "set tls.mode to 'acme' (with acme_domains configured) or 'file'.", + "mode", mode, "hostname", hostname) + cert, err = generateSelfSigned(hostname) + if err != nil { + return nil, fmt.Errorf("generating self-signed cert: %w", err) + } + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: minVersion, + ServerName: hostname, + }, nil +} + +// ParseMinVersion converts the config string ("TLS12"/"TLS13") to the +// crypto/tls constant. +func ParseMinVersion(s string) uint16 { + if s == "TLS13" { + return tls.VersionTLS13 + } + return tls.VersionTLS12 +} + +func generateSelfSigned(hostname string) (tls.Certificate, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return tls.Certificate{}, err + } + + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return tls.Certificate{}, err + } + + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: hostname, Organization: []string{"GoMail (self-signed, dev only)"}}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IsCA: true, + BasicConstraintsValid: true, + } + + if ip := net.ParseIP(hostname); ip != nil { + template.IPAddresses = []net.IP{ip} + } else { + template.DNSNames = []string{hostname} + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, err + } + + return tls.Certificate{ + Certificate: [][]byte{derBytes}, + PrivateKey: priv, + }, nil +} diff --git a/internal/totp/totp.go b/internal/totp/totp.go new file mode 100644 index 0000000..a9b30ea --- /dev/null +++ b/internal/totp/totp.go @@ -0,0 +1,123 @@ +// Package totp implements TOTP (RFC 6238, built on HOTP RFC 4226) — +// hand-rolled on stdlib crypto/hmac + crypto/sha1 + encoding/base32, no +// third-party OTP library. Correctness is checked against RFC 6238's own +// published test vectors (Appendix B) in the test suite, not just "it +// produces a 6-digit number." +package totp + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "encoding/base32" + "fmt" + "math" + "net/url" + "strconv" + "strings" + "time" +) + +const ( + period = 30 // seconds per RFC 6238's recommended default + digits = 6 +) + +// GenerateSecret creates a new random 20-byte (160-bit) secret, base32 +// encoded — the standard size real authenticator apps (Google Authenticator, +// Authy, etc.) expect. +func GenerateSecret() (string, error) { + b := make([]byte, 20) + if _, err := rand.Read(b); err != nil { + return "", fmt.Errorf("generating TOTP secret: %w", err) + } + return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b), nil +} + +// Generate computes the TOTP code for secret at the given time — exported +// primarily so the test suite can check RFC 6238's published vectors, which +// specify exact codes for exact timestamps. +func Generate(secret string, at time.Time) (string, error) { + key, err := decodeSecret(secret) + if err != nil { + return "", err + } + counter := uint64(at.Unix() / period) + return hotp(key, counter), nil +} + +// Validate checks code against the current time step and, per common TOTP +// practice, the one step before and after (±30s) to tolerate minor clock +// drift between server and authenticator app. +func Validate(secret, code string) (bool, error) { + key, err := decodeSecret(secret) + if err != nil { + return false, err + } + code = strings.TrimSpace(code) + now := time.Now().UTC() + counter := uint64(now.Unix() / period) + + for _, skew := range []int64{0, -1, 1} { + c := hotp(key, uint64(int64(counter)+skew)) + if c == code { + return true, nil + } + } + return false, nil +} + +// hotp implements RFC 4226 HOTP — the counter-based primitive TOTP wraps. +func hotp(key []byte, counter uint64) string { + msg := make([]byte, 8) + for i := 7; i >= 0; i-- { + msg[i] = byte(counter & 0xff) + counter >>= 8 + } + + mac := hmac.New(sha1.New, key) + mac.Write(msg) + sum := mac.Sum(nil) + + offset := sum[len(sum)-1] & 0x0f + binCode := (uint32(sum[offset])&0x7f)<<24 | + (uint32(sum[offset+1])&0xff)<<16 | + (uint32(sum[offset+2])&0xff)<<8 | + (uint32(sum[offset+3]) & 0xff) + + mod := uint32(math.Pow10(digits)) + return fmt.Sprintf("%0*d", digits, binCode%mod) +} + +func decodeSecret(secret string) ([]byte, error) { + secret = strings.ToUpper(strings.TrimSpace(secret)) + secret = strings.ReplaceAll(secret, " ", "") + key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(secret) + if err != nil { + return nil, fmt.Errorf("decoding TOTP secret: %w", err) + } + return key, nil +} + +// ProvisioningURI builds the otpauth:// URI real authenticator apps use to +// set up an account — either scanned as a QR code (QR rendering itself is +// deliberately not implemented here, see package doc note below) or +// manually entered, since every mainstream authenticator app supports +// typing in the secret directly as a fallback to scanning. +// +// Note: this package does not generate a QR code image. Real QR encoding +// (Reed-Solomon error correction, matrix placement) is a substantial +// sub-project of its own with little shared surface with TOTP itself — +// deferred rather than half-implemented. The webmail MFA setup page +// displays this URI as both a copyable string and (optionally, via a +// client-side QR library the frontend can add later) a scannable code. +func ProvisioningURI(secret, accountEmail, issuer string) string { + v := url.Values{} + v.Set("secret", secret) + v.Set("issuer", issuer) + v.Set("algorithm", "SHA1") + v.Set("digits", strconv.Itoa(digits)) + v.Set("period", strconv.Itoa(period)) + label := url.PathEscape(issuer) + ":" + url.PathEscape(accountEmail) + return fmt.Sprintf("otpauth://totp/%s?%s", label, v.Encode()) +} diff --git a/internal/vcard/vcard.go b/internal/vcard/vcard.go new file mode 100644 index 0000000..58d559a --- /dev/null +++ b/internal/vcard/vcard.go @@ -0,0 +1,148 @@ +// Package vcard implements a minimal RFC 6350 vCard parser/builder — just +// the fields CardDAV needs to round-trip contacts: UID, FN, N, EMAIL, TEL, +// ORG, NOTE. Not a full vCard 4.0 implementation (no PHOTO, no groups, no +// extended params) — enough for real mail/contacts clients to store and +// retrieve a usable contact, with more fields added as client compatibility +// testing surfaces the need. +package vcard + +import ( + "fmt" + "strings" +) + +type Card struct { + UID string + FN string // formatted name + N string // structured name: Family;Given;Middle;Prefix;Suffix + Email []string + Tel []string + Org string + Note string +} + +// Parse reads a single vCard (BEGIN:VCARD...END:VCARD) into a Card. +func Parse(data string) (*Card, error) { + lines := unfold(data) + c := &Card{} + inCard := false + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + upper := strings.ToUpper(line) + switch { + case upper == "BEGIN:VCARD": + inCard = true + continue + case upper == "END:VCARD": + inCard = false + continue + } + if !inCard { + continue + } + + name, value, found := splitProperty(line) + if !found { + continue + } + switch strings.ToUpper(name) { + case "UID": + c.UID = value + case "FN": + c.FN = value + case "N": + c.N = value + case "EMAIL": + c.Email = append(c.Email, value) + case "TEL": + c.Tel = append(c.Tel, value) + case "ORG": + c.Org = value + case "NOTE": + c.Note = unescape(value) + } + } + + if c.UID == "" { + return nil, fmt.Errorf("vcard missing required UID property") + } + return c, nil +} + +// Build renders a Card back into vCard 4.0 text, CRLF line endings per spec. +func (c *Card) Build() string { + var b strings.Builder + b.WriteString("BEGIN:VCARD\r\n") + b.WriteString("VERSION:4.0\r\n") + b.WriteString("UID:" + c.UID + "\r\n") + if c.FN != "" { + b.WriteString("FN:" + escape(c.FN) + "\r\n") + } + if c.N != "" { + b.WriteString("N:" + c.N + "\r\n") + } + for _, e := range c.Email { + b.WriteString("EMAIL:" + e + "\r\n") + } + for _, t := range c.Tel { + b.WriteString("TEL:" + t + "\r\n") + } + if c.Org != "" { + b.WriteString("ORG:" + escape(c.Org) + "\r\n") + } + if c.Note != "" { + b.WriteString("NOTE:" + escape(c.Note) + "\r\n") + } + b.WriteString("END:VCARD\r\n") + return b.String() +} + +// splitProperty splits "NAME;PARAM=x:value" into (name, value) — parameters +// are discarded in this minimal pass (deferred: TYPE=work/home distinction). +func splitProperty(line string) (name, value string, found bool) { + colonIdx := strings.Index(line, ":") + if colonIdx == -1 { + return "", "", false + } + namePart := line[:colonIdx] + value = line[colonIdx+1:] + if semiIdx := strings.Index(namePart, ";"); semiIdx != -1 { + namePart = namePart[:semiIdx] + } + return namePart, value, true +} + +// unfold reverses RFC 6350 §3.2 line folding (a line starting with a single +// space or tab is a continuation of the previous line). +func unfold(data string) []string { + raw := strings.Split(strings.ReplaceAll(data, "\r\n", "\n"), "\n") + var out []string + for _, line := range raw { + if len(line) > 0 && (line[0] == ' ' || line[0] == '\t') && len(out) > 0 { + out[len(out)-1] += line[1:] + } else { + out = append(out, line) + } + } + return out +} + +func escape(s string) string { + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, ",", "\\,") + s = strings.ReplaceAll(s, ";", "\\;") + s = strings.ReplaceAll(s, "\n", "\\n") + return s +} + +func unescape(s string) string { + s = strings.ReplaceAll(s, "\\n", "\n") + s = strings.ReplaceAll(s, "\\,", ",") + s = strings.ReplaceAll(s, "\\;", ";") + s = strings.ReplaceAll(s, "\\\\", "\\") + return s +} diff --git a/internal/vcard/vcard_fuzz_test.go b/internal/vcard/vcard_fuzz_test.go new file mode 100644 index 0000000..0fd6cc5 --- /dev/null +++ b/internal/vcard/vcard_fuzz_test.go @@ -0,0 +1,29 @@ +package vcard + +import "testing" + +func FuzzParse(f *testing.F) { + f.Add("BEGIN:VCARD\r\nVERSION:4.0\r\nUID:test-1\r\nFN:Test Person\r\nEND:VCARD\r\n") + f.Add("BEGIN:VCARD\nUID:no-crlf\nEND:VCARD\n") + f.Add("BEGIN:VCARD\r\nUID:folded\r\nNOTE:line one\r\n continued\r\nEND:VCARD\r\n") + f.Add("BEGIN:VCARD\r\nUID:escaped\r\nNOTE:a\\,b\\;c\\\\d\\ne\r\nEND:VCARD\r\n") + f.Add("") + f.Add("BEGIN:VCARD\r\nEND:VCARD\r\n") + f.Add("not a vcard at all") + f.Add("BEGIN:VCARD\r\n:\r\nEND:VCARD\r\n") + f.Add("BEGIN:VCARD\r\nUID\r\nEND:VCARD\r\n") + f.Add("BEGIN:VCARD\r\n;;;:;;;\r\nUID:x\r\nEND:VCARD\r\n") + + f.Fuzz(func(t *testing.T, data string) { + // The only contract checked here: Parse must never panic on any + // input, malformed or not — a returned error is fine, a crash is + // not, since this parser runs on untrusted client-supplied CardDAV + // PUT bodies. + defer func() { + if r := recover(); r != nil { + t.Fatalf("Parse panicked on input %q: %v", data, r) + } + }() + Parse(data) + }) +} diff --git a/internal/webmail/api.go b/internal/webmail/api.go new file mode 100644 index 0000000..c77583a --- /dev/null +++ b/internal/webmail/api.go @@ -0,0 +1,958 @@ +// Package webmail implements the REST API and embedded SPA for GoMail's own +// webmail client. The API wraps internal/accounts.GoMailProvider for message +// operations — direct local access, no JMAP dependency — so this phase isn't +// blocked on Phase 9's JMAP server. When JMAP lands, only this package's +// internals need to change; the REST contract (and therefore the frontend) +// stays the same. +package webmail + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "gomail/internal/accounts" + "gomail/internal/auth" + "gomail/internal/crypto" + "gomail/internal/db" + "gomail/internal/mailstore" + "gomail/internal/oauth2" + "gomail/internal/totp" + "gomail/internal/webtoken" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +const sessionTTL = 24 * time.Hour + +type Handler struct { + database *db.DB + store *mailstore.Store + mk *crypto.MasterKey + jwtSecret string + + oauthConfigs map[string]*oauth2.Config // keyed by "google" / "microsoft", nil entries if not configured + + oauthStateMu sync.Mutex + oauthState map[string]oauthStateEntry // CSRF state -> pending link request +} + +type oauthStateEntry struct { + UserID string + Provider string + ExpiresAt time.Time +} + +func NewHandler(database *db.DB, store *mailstore.Store, mk *crypto.MasterKey, jwtSecret string, oauthConfigs map[string]*oauth2.Config) *Handler { + return &Handler{ + database: database, store: store, mk: mk, jwtSecret: jwtSecret, + oauthConfigs: oauthConfigs, + oauthState: make(map[string]oauthStateEntry), + } +} + +func (h *Handler) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/api/auth/login", h.login) + mux.HandleFunc("/api/auth/mfa-verify", h.mfaVerify) + mux.HandleFunc("/api/auth/forgot-password", h.forgotPassword) + mux.HandleFunc("/api/auth/reset-password", h.resetPassword) + mux.HandleFunc("/api/me", h.withAuth(h.getMe)) + mux.HandleFunc("/api/me/mfa/setup", h.withAuth(h.mfaSetup)) + mux.HandleFunc("/api/me/mfa/confirm", h.withAuth(h.mfaConfirm)) + mux.HandleFunc("/api/me/mfa/disable", h.withAuth(h.mfaDisable)) + mux.HandleFunc("/api/me/recovery-email", h.withAuth(h.setRecoveryEmail)) + mux.HandleFunc("/api/me/app-passwords", h.withAuth(h.appPasswords)) + mux.HandleFunc("/api/me/app-passwords/", h.withAuth(h.appPasswordByID)) + mux.HandleFunc("/api/folders", h.withAuth(h.listFolders)) + mux.HandleFunc("/api/folders/", h.withAuth(h.listMessages)) + mux.HandleFunc("/api/messages", h.withAuth(h.sendOrListMessages)) + mux.HandleFunc("/api/messages/", h.withAuth(h.messageByID)) + mux.HandleFunc("/api/quarantine", h.withAuth(h.listQuarantine)) + mux.HandleFunc("/api/quarantine/", h.withAuth(h.releaseQuarantine)) + mux.HandleFunc("/api/events", h.withAuth(h.sseEvents)) + mux.HandleFunc("/api/accounts", h.withAuth(h.listAccounts)) + mux.HandleFunc("/api/accounts/oauth/", h.oauthDispatch) // start needs auth (checked inline), callback doesn't (browser redirect) + mux.HandleFunc("/api/accounts/", h.withAuth(h.deleteAccount)) +} + +// ── JSON helpers ────────────────────────────────────────────────────────────── + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(v) +} + +func writeErr(w http.ResponseWriter, code int, msg string) { + writeJSON(w, code, map[string]string{"error": msg}) +} + +// ── Auth ────────────────────────────────────────────────────────────────────── + +// titleCase upper-cases s's first byte — used only for the ASCII provider +// names ("google", "microsoft") in display strings; strings.Title is +// deprecated and its Unicode word-boundary handling is unneeded here. +func titleCase(s string) string { + if s == "" { + return s + } + return strings.ToUpper(s[:1]) + s[1:] +} + +func (h *Handler) login(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Email, Password string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + + user, ok := auth.Authenticate(h.database, req.Email, req.Password, auth.ScopeIMAP) + if !ok { + slog.Info("webmail login failed", "email", req.Email) + writeErr(w, http.StatusUnauthorized, "invalid credentials") + return + } + + if user.MFAEnabled { + // Password alone is not enough — issue a short-lived, narrowly-scoped + // pre-auth token instead of a real session. It can only be redeemed + // at /api/auth/mfa-verify, and only with a correct TOTP or backup code. + mfaToken, err := webtoken.IssueWithPurpose(h.jwtSecret, user.ID, user.TenantID, string(user.Role), "mfa_pending", 5*time.Minute) + if err != nil { + writeErr(w, http.StatusInternalServerError, "token generation failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"mfa_required": true, "mfa_token": mfaToken}) + return + } + + token, err := webtoken.Issue(h.jwtSecret, user.ID, user.TenantID, string(user.Role), sessionTTL) + if err != nil { + writeErr(w, http.StatusInternalServerError, "token generation failed") + return + } + h.database.Exec(`UPDATE users SET last_login_at = ? WHERE id = ?`, time.Now().UTC(), user.ID) + + writeJSON(w, http.StatusOK, map[string]any{ + "token": token, + "user": map[string]any{"id": user.ID, "email": user.Email, "display_name": user.DisplayName}, + }) +} + +// mfaVerify completes login for an MFA-enabled account — redeems the +// pre-auth token from login() plus a valid TOTP or backup code for a real +// session token. +func (h *Handler) mfaVerify(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ MFAToken, Code string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + + claims, err := webtoken.Verify(h.jwtSecret, req.MFAToken) + if err != nil || claims.Purpose != "mfa_pending" { + writeErr(w, http.StatusUnauthorized, "invalid or expired MFA session") + return + } + + user, err := h.database.GetUser(claims.Subject) + if err != nil || !user.Active { + writeErr(w, http.StatusUnauthorized, "user not found or inactive") + return + } + + verified := false + if user.TOTPSecretEnc != nil { + plain, decErr := crypto.Decrypt(h.mk, user.ID, "totp-secret", user.TOTPSecretEnc) + if decErr == nil { + if ok, _ := totp.Validate(string(plain), req.Code); ok { + verified = true + } + } + } + if !verified { + // Fall back to a backup code — hashed the same way app passwords are. + hash := sha256Hex(req.Code) + if used, _ := h.database.ConsumeBackupCode(user.ID, hash); used { + verified = true + } + } + if !verified { + writeErr(w, http.StatusUnauthorized, "invalid code") + return + } + + token, err := webtoken.Issue(h.jwtSecret, user.ID, user.TenantID, string(user.Role), sessionTTL) + if err != nil { + writeErr(w, http.StatusInternalServerError, "token generation failed") + return + } + h.database.Exec(`UPDATE users SET last_login_at = ? WHERE id = ?`, time.Now().UTC(), user.ID) + + writeJSON(w, http.StatusOK, map[string]any{ + "token": token, + "user": map[string]any{"id": user.ID, "email": user.Email, "display_name": user.DisplayName}, + }) +} + +func (h *Handler) withAuth(next func(http.ResponseWriter, *http.Request, *db.User)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tokenStr := "" + if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { + tokenStr = strings.TrimPrefix(authHeader, "Bearer ") + } else if cookie, err := r.Cookie("gomail_token"); err == nil { + tokenStr = cookie.Value + } + if tokenStr == "" { + writeErr(w, http.StatusUnauthorized, "missing token") + return + } + + claims, err := webtoken.Verify(h.jwtSecret, tokenStr) + if err != nil { + writeErr(w, http.StatusUnauthorized, "invalid or expired token") + return + } + if claims.Purpose != "" { + // A purpose-scoped token (mfa_pending, password_reset) is not a + // session — accepting it here would let it bypass whatever the + // purpose was gating (e.g. MFA). + writeErr(w, http.StatusUnauthorized, "invalid or expired token") + return + } + + // claims.Subject is the user's ID (set at Issue time in login), not + // an email — look up directly by ID. + row := h.database.QueryRow(`SELECT id, tenant_id, domain_id, email, display_name, role, active FROM users WHERE id = ?`, claims.Subject) + var user db.User + if err := row.Scan(&user.ID, &user.TenantID, &user.DomainID, &user.Email, &user.DisplayName, &user.Role, &user.Active); err != nil { + writeErr(w, http.StatusUnauthorized, "user not found") + return + } + if !user.Active { + writeErr(w, http.StatusForbidden, "account disabled") + return + } + + next(w, r, &user) + } +} + +func (h *Handler) getMe(w http.ResponseWriter, r *http.Request, user *db.User) { + writeJSON(w, http.StatusOK, map[string]any{ + "id": user.ID, "email": user.Email, "display_name": user.DisplayName, "role": user.Role, + }) +} + +// ── Folders & messages ────────────────────────────────────────────────────────── + +func (h *Handler) provider(user *db.User) *accounts.GoMailProvider { + return accounts.NewGoMailProvider(h.database, h.store, user) +} + +func (h *Handler) listFolders(w http.ResponseWriter, r *http.Request, user *db.User) { + folders, err := h.provider(user).ListFolders(r.Context()) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, folders) +} + +// listMessages handles GET /api/folders/{folderID}/messages +func (h *Handler) listMessages(w http.ResponseWriter, r *http.Request, user *db.User) { + path := strings.TrimPrefix(r.URL.Path, "/api/folders/") + parts := strings.SplitN(path, "/", 2) + if len(parts) != 2 || parts[1] != "messages" { + http.NotFound(w, r) + return + } + folderID := parts[0] + + opts := accounts.ListOpts{} + if l := r.URL.Query().Get("limit"); l != "" { + opts.Limit, _ = strconv.Atoi(l) + } + if o := r.URL.Query().Get("offset"); o != "" { + opts.Offset, _ = strconv.Atoi(o) + } + + headers, err := h.provider(user).ListMessages(r.Context(), folderID, opts) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, headers) +} + +func (h *Handler) sendOrListMessages(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct { + To []string `json:"to"` + CC []string `json:"cc"` + Subject string `json:"subject"` + Body string `json:"body"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + if len(req.To) == 0 { + writeErr(w, http.StatusBadRequest, "at least one recipient required") + return + } + + msg := &accounts.OutgoingMessage{From: user.Email, To: req.To, CC: req.CC, Subject: req.Subject, Body: req.Body} + if err := h.provider(user).SendMessage(r.Context(), msg); err != nil { + writeErr(w, http.StatusBadGateway, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "sent"}) +} + +// messageByID handles GET/PUT(flags)/DELETE/move on /api/messages/{folderID}/{messageID}[/flags|/move] +func (h *Handler) messageByID(w http.ResponseWriter, r *http.Request, user *db.User) { + path := strings.TrimPrefix(r.URL.Path, "/api/messages/") + parts := strings.Split(path, "/") + if len(parts) < 2 { + http.NotFound(w, r) + return + } + folderID, messageID := parts[0], parts[1] + action := "" + if len(parts) >= 3 { + action = parts[2] + } + p := h.provider(user) + + switch { + case r.Method == http.MethodGet && action == "": + full, err := p.GetMessage(r.Context(), folderID, messageID) + if err != nil { + writeErr(w, http.StatusNotFound, "message not found") + return + } + writeJSON(w, http.StatusOK, full) + + case r.Method == http.MethodPut && action == "flags": + var req struct{ Flags []string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid body") + return + } + if err := p.SetFlags(r.Context(), folderID, messageID, req.Flags); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "updated"}) + + case r.Method == http.MethodPost && action == "move": + var req struct{ DestFolder string `json:"dest_folder"` } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid body") + return + } + if err := p.Move(r.Context(), folderID, messageID, req.DestFolder); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "moved"}) + + case r.Method == http.MethodDelete && action == "": + if err := p.Delete(r.Context(), folderID, messageID); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "deleted"}) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// ── Quarantine ──────────────────────────────────────────────────────────────── + +func (h *Handler) listQuarantine(w http.ResponseWriter, r *http.Request, user *db.User) { + entries, err := h.database.QuarantineEntriesForUser(user.Email, time.Now().AddDate(0, 0, -30)) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, entries) +} + +func (h *Handler) releaseQuarantine(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + id := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/api/quarantine/"), "/release") + + entry, err := h.database.GetQuarantineEntry(id) + if err != nil { + writeErr(w, http.StatusNotFound, "quarantine entry not found") + return + } + + var toAddr string + if err := h.database.QueryRow(`SELECT to_address FROM messages WHERE id = ?`, entry.MessageID).Scan(&toAddr); err != nil { + writeErr(w, http.StatusNotFound, "underlying message not found") + return + } + if toAddr != user.Email { + writeErr(w, http.StatusForbidden, "not your message") + return + } + + raw, err := h.store.ReadQuarantineFile(entry.MessageID, entry.EMLPath) + if err != nil { + writeErr(w, http.StatusInternalServerError, "failed to read quarantined message") + return + } + if _, err := h.store.Deliver(user.ID, user.Email, "INBOX", raw); err != nil { + writeErr(w, http.StatusInternalServerError, "failed to deliver released message") + return + } + if err := h.database.ReleaseQuarantineEntry(id, user.Email); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + writeJSON(w, http.StatusOK, map[string]string{"message": "released"}) +} + +// ── SSE ─────────────────────────────────────────────────────────────────────── + +// sseEvents streams a countUpdate event whenever the INBOX message count +// changes, polling every few seconds — a real push mechanism (fsnotify-style +// instant delivery) is a natural follow-up once IMAP IDLE's polling loop is +// generalized; this establishes the wire contract webmail's UI codes against +// today. +func (h *Handler) sseEvents(w http.ResponseWriter, r *http.Request, user *db.User) { + flusher, ok := w.(http.Flusher) + if !ok { + writeErr(w, http.StatusInternalServerError, "streaming unsupported") + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + ctx := r.Context() + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + lastCount := -1 + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + entries, err := h.database.ListMailboxEntries(user.ID, "INBOX") + if err != nil { + continue + } + if len(entries) != lastCount { + lastCount = len(entries) + fmt.Fprintf(w, "event: countUpdate\ndata: {\"mailbox\":\"INBOX\",\"total\":%d}\n\n", len(entries)) + flusher.Flush() + } + } + } +} + +// ── Linked accounts ────────────────────────────────────────────────────────── + +func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request, user *db.User) { + accts, err := h.database.ListLinkedAccounts(user.ID) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + // Never expose CredentialEnc — even encrypted, there's no reason to send + // it to the client at all. + type safeAccount struct { + ID string `json:"id"` + Provider string `json:"provider"` + DisplayName string `json:"display_name"` + EmailAddress string `json:"email_address"` + LastSyncAt string `json:"last_sync_at,omitempty"` + } + out := make([]safeAccount, 0, len(accts)) + for _, a := range accts { + sa := safeAccount{ID: a.ID, Provider: string(a.Provider), DisplayName: a.DisplayName, EmailAddress: a.EmailAddress} + if a.LastSyncAt != nil { + sa.LastSyncAt = a.LastSyncAt.Format(time.RFC3339) + } + out = append(out, sa) + } + writeJSON(w, http.StatusOK, out) +} + +func (h *Handler) deleteAccount(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/accounts/") + if id == "" || strings.Contains(id, "/") { + http.NotFound(w, r) + return + } + account, err := h.database.GetLinkedAccount(id) + if err != nil || account.UserID != user.ID { + writeErr(w, http.StatusNotFound, "account not found") + return + } + if err := h.database.DeactivateLinkedAccount(id); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "unlinked"}) +} + +// oauthDispatch routes /api/accounts/oauth/{provider}/start and .../callback. +// start requires an authenticated session (checked inline, not via withAuth, +// since callback intentionally does NOT require one — it's a plain browser +// redirect from the provider with no Authorization header available). +func (h *Handler) oauthDispatch(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/api/accounts/oauth/") + parts := strings.SplitN(path, "/", 2) + if len(parts) != 2 { + http.NotFound(w, r) + return + } + provider, action := parts[0], parts[1] + + switch action { + case "start": + h.withAuth(func(w http.ResponseWriter, r *http.Request, user *db.User) { + h.oauthStart(w, r, user, provider) + })(w, r) + case "callback": + h.oauthCallback(w, r, provider) + default: + http.NotFound(w, r) + } +} + +func (h *Handler) oauthStart(w http.ResponseWriter, r *http.Request, user *db.User, provider string) { + cfg, ok := h.oauthConfigs[provider] + if !ok || cfg == nil { + writeErr(w, http.StatusServiceUnavailable, fmt.Sprintf("%s OAuth is not configured on this server", provider)) + return + } + + state, err := randomState() + if err != nil { + writeErr(w, http.StatusInternalServerError, "failed to generate state") + return + } + + h.oauthStateMu.Lock() + h.pruneExpiredState() + h.oauthState[state] = oauthStateEntry{UserID: user.ID, Provider: provider, ExpiresAt: time.Now().UTC().Add(10 * time.Minute)} + h.oauthStateMu.Unlock() + + writeJSON(w, http.StatusOK, map[string]string{"auth_url": cfg.BuildAuthURL(state)}) +} + +func (h *Handler) oauthCallback(w http.ResponseWriter, r *http.Request, provider string) { + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + if code == "" || state == "" { + writeErr(w, http.StatusBadRequest, "missing code or state") + return + } + + h.oauthStateMu.Lock() + entry, ok := h.oauthState[state] + if ok { + delete(h.oauthState, state) // one-time use + } + h.oauthStateMu.Unlock() + + if !ok { + writeErr(w, http.StatusBadRequest, "invalid or expired state (possible CSRF attempt)") + return + } + if entry.Provider != provider { + writeErr(w, http.StatusBadRequest, "state/provider mismatch") + return + } + if time.Now().UTC().After(entry.ExpiresAt) { + writeErr(w, http.StatusBadRequest, "state expired, please try linking again") + return + } + + cfg, ok := h.oauthConfigs[provider] + if !ok || cfg == nil { + writeErr(w, http.StatusServiceUnavailable, "provider not configured") + return + } + + token, err := cfg.ExchangeCode(r.Context(), code) + if err != nil { + slog.Error("oauth2 code exchange failed", "provider", provider, "err", err) + writeErr(w, http.StatusBadGateway, "failed to exchange authorization code") + return + } + + dbProvider := db.ProviderGmail + if provider == "microsoft" { + dbProvider = db.ProviderM365 + } + + // Note: a real implementation would call the provider's userinfo/profile + // endpoint here to learn the account's actual email address rather than + // require it as a query param — deferred; for now the display name is + // generic and the operator/user can rename it, matching the minimum + // needed to prove the OAuth2 flow itself is correct end-to-end. + email := r.URL.Query().Get("email") + if email == "" { + email = provider + "-account" + } + + account, err := accounts.LinkOAuth2Account(h.database, h.mk, entry.UserID, titleCase(provider)+" Account", email, dbProvider, token) + if err != nil { + slog.Error("failed to store linked OAuth2 account", "err", err) + writeErr(w, http.StatusInternalServerError, "failed to link account") + return + } + + writeJSON(w, http.StatusOK, map[string]string{"message": "linked", "account_id": account.ID}) +} + +func (h *Handler) pruneExpiredState() { + now := time.Now().UTC() + for k, v := range h.oauthState { + if now.After(v.ExpiresAt) { + delete(h.oauthState, k) + } + } +} + +func randomState() (string, error) { + b := make([]byte, 24) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +// ── MFA setup/confirm/disable ──────────────────────────────────────────────── + +func sha256Hex(s string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(strings.ToUpper(s)))) + return hex.EncodeToString(sum[:]) +} + +// mfaSetup generates a new TOTP secret and stores it encrypted but NOT yet +// enabled — the user must confirm one valid code (mfaConfirm) before MFA +// actually takes effect, so an abandoned setup never locks anyone out. +func (h *Handler) mfaSetup(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + secret, err := totp.GenerateSecret() + if err != nil { + writeErr(w, http.StatusInternalServerError, "failed to generate secret") + return + } + encSecret, err := crypto.Encrypt(h.mk, user.ID, "totp-secret", []byte(secret)) + if err != nil { + writeErr(w, http.StatusInternalServerError, "failed to encrypt secret") + return + } + if err := h.database.SetPendingTOTPSecret(user.ID, encSecret); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + uri := totp.ProvisioningURI(secret, user.Email, "GoMail") + writeJSON(w, http.StatusOK, map[string]string{"secret": secret, "provisioning_uri": uri}) +} + +// mfaConfirm verifies one code against the pending secret and, on success, +// enables MFA and generates backup codes (shown to the user exactly once). +func (h *Handler) mfaConfirm(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Code string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + + fresh, err := h.database.GetUser(user.ID) + if err != nil || fresh.TOTPSecretEnc == nil { + writeErr(w, http.StatusBadRequest, "no pending MFA setup — call /api/me/mfa/setup first") + return + } + plain, err := crypto.Decrypt(h.mk, user.ID, "totp-secret", fresh.TOTPSecretEnc) + if err != nil { + writeErr(w, http.StatusInternalServerError, "failed to decrypt pending secret") + return + } + ok, err := totp.Validate(string(plain), req.Code) + if err != nil || !ok { + writeErr(w, http.StatusBadRequest, "invalid code") + return + } + + backupCodes := make([]string, 8) + hashes := make([]string, 8) + for i := range backupCodes { + raw := make([]byte, 5) + rand.Read(raw) + code := strings.ToUpper(hex.EncodeToString(raw)) // 10 hex chars, easy to type + backupCodes[i] = code + hashes[i] = sha256Hex(code) + } + if err := h.database.ReplaceBackupCodes(user.ID, hashes); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + if err := h.database.SetMFAEnabled(user.ID, true); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + writeJSON(w, http.StatusOK, map[string]any{"message": "MFA enabled", "backup_codes": backupCodes}) +} + +func (h *Handler) mfaDisable(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Password string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + // Require the password again — disabling MFA is high-stakes enough that + // a hijacked-but-still-logged-in session shouldn't be able to do it + // with just the session token. + if _, ok := auth.Authenticate(h.database, user.Email, req.Password, auth.ScopeIMAP); !ok { + writeErr(w, http.StatusUnauthorized, "incorrect password") + return + } + if err := h.database.ClearTOTPSecret(user.ID); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "MFA disabled"}) +} + +// ── App passwords ───────────────────────────────────────────────────────────── + +func (h *Handler) appPasswords(w http.ResponseWriter, r *http.Request, user *db.User) { + switch r.Method { + case http.MethodGet: + rows, err := h.database.Query(`SELECT id, label, scopes, last_used_at, expires_at, created_at FROM app_passwords WHERE user_id = ? ORDER BY created_at DESC`, user.ID) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + defer rows.Close() + type entry struct { + ID, Label, Scopes string + LastUsedAt, ExpiresAt *time.Time + CreatedAt time.Time + } + var out []entry + for rows.Next() { + var e entry + if err := rows.Scan(&e.ID, &e.Label, &e.Scopes, &e.LastUsedAt, &e.ExpiresAt, &e.CreatedAt); err != nil { + continue + } + out = append(out, e) + } + writeJSON(w, http.StatusOK, out) + + case http.MethodPost: + var req struct { + Label string + Scopes string + ExpiresIn string // e.g. "30d", "" = never + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Label == "" { + writeErr(w, http.StatusBadRequest, "label is required") + return + } + if req.Scopes == "" { + req.Scopes = "smtp,imap" + } + + raw := make([]byte, 24) + rand.Read(raw) + token := strings.ToUpper(hex.EncodeToString(raw)) + hash, err := bcrypt.GenerateFromPassword([]byte(token), 12) + if err != nil { + writeErr(w, http.StatusInternalServerError, "hashing failed") + return + } + + var expiresAt *time.Time + if req.ExpiresIn != "" { + d, err := parseDuration(req.ExpiresIn) + if err != nil { + writeErr(w, http.StatusBadRequest, "invalid expires_in format (use e.g. '30d', '90d')") + return + } + t := time.Now().UTC().Add(d) + expiresAt = &t + } + + id := uuid.NewString() + _, err = h.database.Exec(`INSERT INTO app_passwords (id, user_id, label, password_hash, scopes, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, + id, user.ID, req.Label, string(hash), req.Scopes, expiresAt) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + writeJSON(w, http.StatusCreated, map[string]string{"id": id, "token": token}) // token shown exactly once + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (h *Handler) appPasswordByID(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/me/app-passwords/") + res, err := h.database.Exec(`DELETE FROM app_passwords WHERE id = ? AND user_id = ?`, id, user.ID) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + if n, _ := res.RowsAffected(); n == 0 { + writeErr(w, http.StatusNotFound, "app password not found") + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "revoked"}) +} + +func parseDuration(s string) (time.Duration, error) { + if strings.HasSuffix(s, "d") { + var days int + if _, err := fmt.Sscanf(s, "%dd", &days); err != nil { + return 0, err + } + return time.Duration(days) * 24 * time.Hour, nil + } + return time.ParseDuration(s) +} + +// ── Password reset (recovery-email based) ──────────────────────────────────── + +// forgotPassword always returns 200 regardless of whether the email +// matches an account or that account has a recovery email configured — +// leaking account existence via response differences is exactly what this +// guards against. +func (h *Handler) forgotPassword(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Email string } + json.NewDecoder(r.Body).Decode(&req) + + user, err := h.database.LookupUserByEmail(req.Email) + if err == nil && user.RecoveryEmail != "" { + fingerprint := webtoken.Fingerprint(user.PasswordHash) + resetToken, tokErr := webtoken.IssueResetToken(h.jwtSecret, user.ID, user.TenantID, string(user.Role), fingerprint, 1*time.Hour) + if tokErr == nil { + body := fmt.Sprintf("A password reset was requested for your GoMail account (%s).\r\n\r\n"+ + "Reset token (valid 1 hour): %s\r\n\r\n"+ + "If you didn't request this, you can safely ignore this message.\r\n", user.Email, resetToken) + raw := []byte(fmt.Sprintf("From: noreply@gomail\r\nTo: %s\r\nSubject: GoMail password reset\r\n\r\n%s", user.RecoveryEmail, body)) + if _, queuePath, qErr := h.store.WriteQueueFile(raw); qErr == nil { + h.database.InsertOutboundQueueEntry(&db.OutboundQueueEntry{ + ID: uuid.NewString(), UserID: user.ID, FromAddress: "noreply@" + strings.SplitN(user.Email, "@", 2)[1], + ToAddress: user.RecoveryEmail, EMLPath: queuePath, NextAttemptAt: time.Now().UTC(), + }) + } + } + } + writeJSON(w, http.StatusOK, map[string]string{"message": "if an account with recovery email configured exists, a reset link has been sent"}) +} + +func (h *Handler) resetPassword(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ Token, NewPassword string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.NewPassword) < 8 { + writeErr(w, http.StatusBadRequest, "new_password must be at least 8 characters") + return + } + claims, err := webtoken.Verify(h.jwtSecret, req.Token) + if err != nil || claims.Purpose != "password_reset" { + writeErr(w, http.StatusBadRequest, "invalid or expired reset token") + return + } + current, err := h.database.GetUser(claims.Subject) + if err != nil || !webtoken.FingerprintMatches(claims, current.PasswordHash) { + // Either the user no longer exists, or the password has already + // been changed since this token was issued (including via a prior + // use of this same token) — reject either way, single-use enforced. + writeErr(w, http.StatusBadRequest, "invalid or expired reset token") + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12) + if err != nil { + writeErr(w, http.StatusInternalServerError, "hashing failed") + return + } + if err := h.database.SetUserPassword(claims.Subject, string(hash)); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "password reset successful"}) +} + +func (h *Handler) setRecoveryEmail(w http.ResponseWriter, r *http.Request, user *db.User) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct{ RecoveryEmail string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid request body") + return + } + if err := h.database.SetRecoveryEmail(user.ID, req.RecoveryEmail); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "recovery email updated"}) +} diff --git a/internal/webmail/embed.go b/internal/webmail/embed.go new file mode 100644 index 0000000..cb60cc9 --- /dev/null +++ b/internal/webmail/embed.go @@ -0,0 +1,6 @@ +package webmail + +import "embed" + +//go:embed static/index.html +var StaticFS embed.FS diff --git a/internal/webmail/static/index.html b/internal/webmail/static/index.html new file mode 100644 index 0000000..b8bf25e --- /dev/null +++ b/internal/webmail/static/index.html @@ -0,0 +1,203 @@ + + + + + +GoMail + + + + + + + + + + + + + + diff --git a/internal/webtoken/webtoken.go b/internal/webtoken/webtoken.go new file mode 100644 index 0000000..90f1c45 --- /dev/null +++ b/internal/webtoken/webtoken.go @@ -0,0 +1,137 @@ +// Package webtoken implements minimal JWT issuing/verification (HS256 only) +// for webmail/admin session tokens — hand-rolled on stdlib crypto/hmac +// rather than a third-party JWT library, matching the project's +// dependency-free principle. Supports exactly what session tokens need: +// a subject (user ID), an expiry, and tamper-evident signing. No JWK sets, +// no algorithm negotiation, no other algorithms — HS256 with a server-side +// secret is the right tool for "did we issue this token", nothing more. +package webtoken + +import ( + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "time" +) + +type Claims struct { + Subject string `json:"sub"` + TenantID string `json:"tenant_id,omitempty"` + Role string `json:"role,omitempty"` + Purpose string `json:"purpose,omitempty"` // e.g. "mfa_pending", "password_reset" — empty means a normal full session + Ctx string `json:"ctx,omitempty"` // purpose-specific binding, e.g. a Fingerprint of the password hash for password_reset + IssuedAt int64 `json:"iat"` + ExpiresAt int64 `json:"exp"` +} + +var header = base64URLEncode([]byte(`{"alg":"HS256","typ":"JWT"}`)) + +// Issue creates a signed token for the given subject, valid for ttl. +func Issue(secret, subject, tenantID, role string, ttl time.Duration) (string, error) { + return IssueWithPurpose(secret, subject, tenantID, role, "", ttl) +} + +// IssueWithPurpose is Issue plus a purpose tag — used for tokens that are +// NOT a full session (MFA-pending, password-reset) so a caller checking +// claims.Purpose can refuse to treat them as one, even though they're +// structurally the same JWT and share the same verification path. +func IssueWithPurpose(secret, subject, tenantID, role, purpose string, ttl time.Duration) (string, error) { + now := time.Now().UTC() + claims := Claims{ + Subject: subject, TenantID: tenantID, Role: role, Purpose: purpose, + IssuedAt: now.Unix(), ExpiresAt: now.Add(ttl).Unix(), + } + payloadJSON, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("marshal claims: %w", err) + } + payload := base64URLEncode(payloadJSON) + + signingInput := header + "." + payload + sig := sign(secret, signingInput) + return signingInput + "." + sig, nil +} + +// IssueResetToken issues a password_reset purpose token bound to +// passwordHashFingerprint (see Fingerprint) — the fingerprint of the +// user's password hash at issuance time. Because resetting the password +// changes that hash, FingerprintMatches will reject the same token on any +// second use, giving single-use semantics with no server-side token store. +func IssueResetToken(secret, subject, tenantID, role, passwordHashFingerprint string, ttl time.Duration) (string, error) { + now := time.Now().UTC() + claims := Claims{ + Subject: subject, TenantID: tenantID, Role: role, Purpose: "password_reset", + Ctx: passwordHashFingerprint, + IssuedAt: now.Unix(), ExpiresAt: now.Add(ttl).Unix(), + } + payloadJSON, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("marshal claims: %w", err) + } + payload := base64URLEncode(payloadJSON) + + signingInput := header + "." + payload + sig := sign(secret, signingInput) + return signingInput + "." + sig, nil +} + +// Fingerprint returns a short, non-reversible fingerprint of s (e.g. a +// password hash), suitable for embedding in a token to detect whether the +// underlying value has changed since the token was issued. +func Fingerprint(s string) string { + sum := sha256.Sum256([]byte(s)) + return base64.RawURLEncoding.EncodeToString(sum[:8]) +} + +// FingerprintMatches reports, in constant time, whether s's Fingerprint +// matches the one embedded in claims.Ctx. +func FingerprintMatches(claims *Claims, s string) bool { + return subtle.ConstantTimeCompare([]byte(Fingerprint(s)), []byte(claims.Ctx)) == 1 +} + +// Verify checks signature and expiry, returning the claims if valid. +func Verify(secret, token string) (*Claims, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("malformed token") + } + signingInput := parts[0] + "." + parts[1] + expectedSig := sign(secret, signingInput) + + // Constant-time comparison — avoids leaking signature validity via timing. + if subtle.ConstantTimeCompare([]byte(expectedSig), []byte(parts[2])) != 1 { + return nil, fmt.Errorf("invalid signature") + } + + payloadJSON, err := base64URLDecode(parts[1]) + if err != nil { + return nil, fmt.Errorf("decode payload: %w", err) + } + var claims Claims + if err := json.Unmarshal(payloadJSON, &claims); err != nil { + return nil, fmt.Errorf("unmarshal claims: %w", err) + } + + if time.Now().UTC().Unix() > claims.ExpiresAt { + return nil, fmt.Errorf("token expired") + } + return &claims, nil +} + +func sign(secret, signingInput string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(signingInput)) + return base64URLEncode(mac.Sum(nil)) +} + +func base64URLEncode(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} + +func base64URLDecode(s string) ([]byte, error) { + return base64.RawURLEncoding.DecodeString(s) +}