From 8ba3d1381d6b988402355f9516f580038ee0cf87 Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Sun, 20 Nov 2022 04:32:57 -0600 Subject: [PATCH] Add lsimap troubleshooting tool The lsimap binary is intended to provide a simple CLI tool to list advertised IMAP server capabilities. As part of implementing this tool some minor tweaks were made to the config and mbxs packages to mute some existing log messages by default. Equivalent log output was added to the check_imap_mailbox and list-emails tools to compensate for the change. README updated with examples for lsimap tool. --- .gitignore | 1 + Makefile | 2 +- README.md | 94 ++++++++++++++++++++++++++++++-- cmd/check_imap_mailbox/main.go | 1 + cmd/list-emails/main.go | 1 + cmd/lsimap/doc.go | 18 +++++++ cmd/lsimap/main.go | 98 ++++++++++++++++++++++++++++++++++ cmd/lsimap/winres/winres.json | 53 ++++++++++++++++++ internal/config/config.go | 9 ++++ internal/config/flags.go | 10 +++- internal/config/logging.go | 42 ++++++++------- internal/config/validate.go | 34 +++++++++--- internal/mbxs/connect.go | 9 ++-- 13 files changed, 334 insertions(+), 38 deletions(-) create mode 100644 cmd/lsimap/doc.go create mode 100644 cmd/lsimap/main.go create mode 100644 cmd/lsimap/winres/winres.json diff --git a/.gitignore b/.gitignore index 04934fee..caf264c1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ # Ignore binaries at root of repo (often generated here when testing) /check_imap_mailbox /list-emails +/lsimap # Help prevent accidentally including this credentials file in the repo /accounts.ini diff --git a/Makefile b/Makefile index f353a901..f75b0333 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ SHELL = /bin/bash # Space-separated list of cmd/BINARY_NAME directories to build -WHAT = check_imap_mailbox list-emails +WHAT = check_imap_mailbox list-emails lsimap # TODO: This will need to be standardized across all cmd files in order to # work as intended. diff --git a/README.md b/README.md index 110dee41..ab1c21c8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Various tools used to monitor mail services - [Features](#features) - [`check_imap_mailbox`](#check_imap_mailbox) - [`list-emails`](#list-emails) + - [`lsimap`](#lsimap) - [Requirements](#requirements) - [Building source code](#building-source-code) - [Running](#running) @@ -32,6 +33,8 @@ Various tools used to monitor mail services - [Configuration file](#configuration-file) - [Settings](#settings) - [Usage](#usage) + - [`lsimap`](#lsimap-1) + - [Command-line arguments](#command-line-arguments-2) - [Examples](#examples) - [`check_imap_mailbox`](#check_imap_mailbox-2) - [As a Nagios plugin](#as-a-nagios-plugin) @@ -39,6 +42,7 @@ Various tools used to monitor mail services - [`list-emails`](#list-emails-2) - [No options](#no-options) - [Alternate locations for config file, log and report directories](#alternate-locations-for-config-file-log-and-report-directories) + - [`lsimap`](#lsimap-2) - [License](#license) - [References](#references) @@ -51,10 +55,11 @@ submit improvements for review and potential inclusion into the project. This repo contains various tools used to monitor mail services. -| Tool Name | Overall Status | Description | -| -------------------- | -------------- | ---------------------------------------------------------- | -| `check_imap_mailbox` | Stable | Nagios plugin used to monitor mailboxes for items | -| `list-emails` | Stable | Small CLI app used to generate listing of mailbox contents | +| Tool Name | Overall Status | Description | +| -------------------- | -------------- | ------------------------------------------------------------------------ | +| `check_imap_mailbox` | Stable | Nagios plugin used to monitor mailboxes for items | +| `list-emails` | Stable | Small CLI app used to generate listing of mailbox contents | +| `lsimap` | Alpha | Small CLI tool to list advertised capabilities for specified IMAP server | ## Features @@ -102,6 +107,21 @@ This repo contains various tools used to monitor mail services. - the intent is to help prevent MySQL errors when posting summary reports - e.g., `ERROR 1366 (22007): Incorrect string value` +### `lsimap` + +- Quick one-off tool to list advertised capabilities for specified IMAP server +- Leveled logging + - `console writer`: human-friendly, colorized output + - choice of `disabled`, `panic`, `fatal`, `error`, `warn`, `info` (the + default), `debug` or `trace` + - enable `debug` level to monitor submitted IMAP commands and received IMAP + server responses +- TLS IMAP4 connectivity + - port defaults to 993/tcp + - network type defaults to either of IPv4 and IPv6, but optionally limited + to IPv4-only or IPv6-only + - user-specified minimum TLS version + ## Requirements The following is a loose guideline. Other combinations of Go and operating @@ -288,6 +308,24 @@ You may also place the file wherever you like and refer to it using the `-config-file` (full-length flag name). See the [Examples](#examples) and [Command-line arguments](#command-line-arguments) sections for usage details. +### `lsimap` + +#### Command-line arguments + +- Flags marked as **`required`** must be set via CLI flag. +- Flags *not* marked as required are for settings where a useful default is + already defined. + +| Option | Required | Default | Repeat | Possible | Description | +| --------------- | -------- | -------------- | ------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `h`, `help` | No | | No | `-h`, `--help` | Generate listing of all valid command-line options and applicable (short) guidance for using them. | +| `server` | Yes | *empty string* | No | *valid FQDN or IP Address* | The fully-qualified domain name of the remote mail server. | +| `port` | No | `993` | No | *valid IMAP TCP port* | TCP port used to connect to the remote mail server. This is usually the same port used for TLS encrypted IMAP connections. | +| `net-type` | No | `auto` | No | `auto`, `tcp4`, `tcp6` | Limits network connections to remote mail servers to one of the specified types. | +| `min-tls` | No | `tls12` | No | `tls10`, `tls11`, `tls12`, `tls13` | Limits version of TLS used for connections to remote mail servers. | +| `logging-level` | No | `info` | No | `disabled`, `panic`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` | Sets log level. | +| `version` | No | `false` | No | `true`, `false` | Whether to display application version and then immediately exit application | + ## Examples ### `check_imap_mailbox` @@ -351,6 +389,54 @@ Checking account: email2 OK: Successfully generated reports for accounts: email1@example.com, email2@example.com ``` +### `lsimap` + +Quick listings for outlook.office365.com and imap.gmail.com. + +This tool can be useful for determining at a glance what authentication +mechanisms are supported by an IMAP server. + +```console +$ ./lsimap --server outlook.office365.com +6:10AM INF cmd\lsimap\main.go:61 > Connection established to server +6:10AM INF cmd\lsimap\main.go:70 > Gathering pre-login capabilities +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=PLAIN +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=XOAUTH2 +6:10AM INF cmd\lsimap\main.go:87 > Capability: CHILDREN +6:10AM INF cmd\lsimap\main.go:87 > Capability: ID +6:10AM INF cmd\lsimap\main.go:87 > Capability: IDLE +6:10AM INF cmd\lsimap\main.go:87 > Capability: IMAP4 +6:10AM INF cmd\lsimap\main.go:87 > Capability: IMAP4rev1 +6:10AM INF cmd\lsimap\main.go:87 > Capability: LITERAL+ +6:10AM INF cmd\lsimap\main.go:87 > Capability: MOVE +6:10AM INF cmd\lsimap\main.go:87 > Capability: NAMESPACE +6:10AM INF cmd\lsimap\main.go:87 > Capability: SASL-IR +6:10AM INF cmd\lsimap\main.go:87 > Capability: UIDPLUS +6:10AM INF cmd\lsimap\main.go:87 > Capability: UNSELECT +6:10AM INF cmd\lsimap\main.go:95 > Connection to server closed + +$ ./lsimap --server imap.gmail.com +6:10AM INF cmd\lsimap\main.go:61 > Connection established to server +6:10AM INF cmd\lsimap\main.go:70 > Gathering pre-login capabilities +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=OAUTHBEARER +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=PLAIN +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=PLAIN-CLIENTTOKEN +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=XOAUTH +6:10AM INF cmd\lsimap\main.go:87 > Capability: AUTH=XOAUTH2 +6:10AM INF cmd\lsimap\main.go:87 > Capability: CHILDREN +6:10AM INF cmd\lsimap\main.go:87 > Capability: ID +6:10AM INF cmd\lsimap\main.go:87 > Capability: IDLE +6:10AM INF cmd\lsimap\main.go:87 > Capability: IMAP4rev1 +6:10AM INF cmd\lsimap\main.go:87 > Capability: NAMESPACE +6:10AM INF cmd\lsimap\main.go:87 > Capability: QUOTA +6:10AM INF cmd\lsimap\main.go:87 > Capability: SASL-IR +6:10AM INF cmd\lsimap\main.go:87 > Capability: UNSELECT +6:10AM INF cmd\lsimap\main.go:87 > Capability: X-GM-EXT-1 +6:10AM INF cmd\lsimap\main.go:87 > Capability: XLIST +6:10AM INF cmd\lsimap\main.go:87 > Capability: XYZZY +6:10AM INF cmd\lsimap\main.go:95 > Connection to server closed +``` + ## License From the [LICENSE](LICENSE) file: diff --git a/cmd/check_imap_mailbox/main.go b/cmd/check_imap_mailbox/main.go index 40326aef..eb8b7fbd 100644 --- a/cmd/check_imap_mailbox/main.go +++ b/cmd/check_imap_mailbox/main.go @@ -81,6 +81,7 @@ func main() { return } + logger.Debug().Msg("Connection established to server") if loginErr := mbxs.Login(c, account.Username, account.Password, logger); loginErr != nil { logger.Error().Err(loginErr).Msg("Login error occurred") diff --git a/cmd/list-emails/main.go b/cmd/list-emails/main.go index bb9314ce..e407ea5c 100644 --- a/cmd/list-emails/main.go +++ b/cmd/list-emails/main.go @@ -100,6 +100,7 @@ func main() { return } + logger.Info().Msg("Connection established to server") if loginErr := mbxs.Login(c, account.Username, account.Password, logger); loginErr != nil { logger.Error().Err(loginErr).Msg("failed to login to server") diff --git a/cmd/lsimap/doc.go b/cmd/lsimap/doc.go new file mode 100644 index 00000000..5f4be437 --- /dev/null +++ b/cmd/lsimap/doc.go @@ -0,0 +1,18 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/check-mail +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +// Small CLI tool to list advertised capabilities for specified IMAP server. +// +// See our [GitHub repo]: +// +// - to review documentation (including examples) +// - for the latest code +// - to file an issue or submit improvements for review and potential +// inclusion into the project +// +// [GitHub repo]: https://github.com/atc0005/check-mail +package main diff --git a/cmd/lsimap/main.go b/cmd/lsimap/main.go new file mode 100644 index 00000000..59b0d7f1 --- /dev/null +++ b/cmd/lsimap/main.go @@ -0,0 +1,98 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/check-mail +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +//go:generate go-winres make --product-version=git-tag --file-version=git-tag + +package main + +import ( + "errors" + "fmt" + "os" + "sort" + + "github.com/atc0005/check-mail/internal/config" + "github.com/atc0005/check-mail/internal/mbxs" + "github.com/rs/zerolog" +) + +func main() { + + // Setup configuration by parsing user-provided flags + cfg, cfgErr := config.New(config.AppType{InspectorIMAPCaps: true}) + switch { + case errors.Is(cfgErr, config.ErrVersionRequested): + fmt.Println(config.Version()) + + return + + case cfgErr != nil: + + // We make some assumptions when setting up our logger as we do not + // have a working configuration based on sysadmin-specified choices. + consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} + logger := zerolog.New(consoleWriter).With().Timestamp().Caller().Logger() + + logger.Err(cfgErr).Msg("Error initializing application") + + return + } + + logger := cfg.Log.With().Logger() + + // We're reusing the common "Accounts" field (and flags) in order to + // obtain specified server and port values vs adding standalone fields and + // flags; this is effectively a loop of 1 iteration (at least for now). At + // some point we may expand the scope of this tool to handle evaluating + // mail servers for a collection of accounts, so using a workflow intended + // for collections is probably appropriate. + for _, account := range cfg.Accounts { + + // Open connection to IMAP server + c, err := mbxs.Connect(account.Server, account.Port, cfg.NetworkType, cfg.MinTLSVersion(), logger) + if err != nil { + logger.Error().Err(err).Msg("error connecting to server") + os.Exit(1) + } + logger.Info().Msg("Connection established to server") + + // Enable client network command/response logging if global logging + // level indicates user wishes to see verbose details. + if zerolog.GlobalLevel() == zerolog.DebugLevel || + zerolog.GlobalLevel() == zerolog.TraceLevel { + c.SetDebug(&logger) + } + + logger.Info().Msg("Gathering pre-login capabilities") + capabilities, err := c.Capability() + if err != nil { + logger.Error().Err(err).Msg("Unable to list server capabilities") + os.Exit(1) + } + + caps := make([]string, 0, len(capabilities)) + for k, v := range capabilities { + if v { + caps = append(caps, k) + } + } + + sort.Strings(caps) + // logger.Info().Msgf("Capabilities: %v", caps) + for _, capability := range caps { + logger.Info().Msgf("Capability: %v", capability) + } + + logger.Debug().Msg("Closing connection to server") + if err := c.Logout(); err != nil { + logger.Error().Err(err).Msg("failed to close connection to server") + os.Exit(1) + } + logger.Info().Msg("Connection to server closed") + + } +} diff --git a/cmd/lsimap/winres/winres.json b/cmd/lsimap/winres/winres.json new file mode 100644 index 00000000..bb02605a --- /dev/null +++ b/cmd/lsimap/winres/winres.json @@ -0,0 +1,53 @@ +{ + "RT_MANIFEST": { + "#1": { + "0409": { + "identity": { + "name": "", + "version": "" + }, + "description": "Small CLI tool to list advertised capabilities for specified IMAP server", + "minimum-os": "win7", + "execution-level": "as invoker", + "ui-access": false, + "auto-elevate": false, + "dpi-awareness": "system", + "disable-theming": false, + "disable-window-filtering": false, + "high-resolution-scrolling-aware": false, + "ultra-high-resolution-scrolling-aware": false, + "long-path-aware": false, + "printer-driver-isolation": false, + "gdi-scaling": false, + "segment-heap": false, + "use-common-controls-v6": false + } + } + }, + "RT_VERSION": { + "#1": { + "0000": { + "fixed": { + "file_version": "0.0.0.0", + "product_version": "0.0.0.0" + }, + "info": { + "0409": { + "Comments": "Part of the atc0005/check-mail project", + "CompanyName": "github.com/atc0005", + "FileDescription": "Small CLI tool to list advertised capabilities for specified IMAP server", + "FileVersion": "", + "InternalName": "lsimap", + "LegalCopyright": "© Adam Chalkley. Licensed under MIT.", + "LegalTrademarks": "", + "OriginalFilename": "main.go", + "PrivateBuild": "", + "ProductName": "check-mail", + "ProductVersion": "", + "SpecialBuild": "" + } + } + } + } + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 0ec15dfd..e0bb6e9e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -73,6 +73,15 @@ type AppType struct { // // Basic Authentication is used to login. ReporterIMAPMailboxBasicAuth bool + + // InspectorIMAPCaps represents an application used for one-off or + // isolated checks of an IMAP server's advertised capabilities. + // + // Unlike a monitoring plugin which is focused on specific attributes + // resulting in a severity-based outcome, an Inspector application is + // intended for examining a small set of targets for + // informational/troubleshooting purposes. + InspectorIMAPCaps bool } // MailAccount represents an email account listed within a configuration file. diff --git a/internal/config/flags.go b/internal/config/flags.go index be4af19d..20c66ce7 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -24,14 +24,20 @@ func (c *Config) handleFlagsConfig(appType AppType) { flag.StringVar(&c.NetworkType, "net-type", defaultNetworkType, networkTypeFlagHelp) flag.StringVar(&c.minTLSVersion, "min-tls", defaultMinTLSVersion, minTLSVersionFlagHelp) - // currently only applies to list-emails app, don't expose to Nagios plugin + // Only applies to Reporter app if appType.ReporterIMAPMailboxBasicAuth { flag.StringVar(&c.ConfigFile, "config-file", defaultINIConfigFileName, iniConfigFileFlagHelp) flag.StringVar(&c.ReportFileOutputDir, "report-file-dir", defaultReportFileOutputDir, reportFileOutputDirFlagHelp) flag.StringVar(&c.LogFileOutputDir, "log-file-dir", defaultLogFileOutputDir, logFileOutputDirFlagHelp) } - // currently only applies to Nagios plugin + // Inspector app + if appType.InspectorIMAPCaps { + flag.StringVar(&account.Server, "server", defaultServer, serverFlagHelp) + flag.IntVar(&account.Port, "port", defaultPort, portFlagHelp) + } + + // Basic Auth Plugin if appType.PluginIMAPMailboxBasicAuth { flag.Var(&account.Folders, "folders", foldersFlagHelp) flag.StringVar(&account.Username, "username", defaultUsername, usernameFlagHelp) diff --git a/internal/config/logging.go b/internal/config/logging.go index 483cb845..74f38cf5 100644 --- a/internal/config/logging.go +++ b/internal/config/logging.go @@ -100,14 +100,11 @@ func (c *Config) setupLogging(appType AppType) error { var logOutput io.Writer - var useLogFile bool switch { // we want to log to a file only for list-emails case appType.ReporterIMAPMailboxBasicAuth: - useLogFile = true - logFilename := fmt.Sprintf( logFilenameTemplate, time.Now().Format(logFilenameDateLayout), @@ -135,13 +132,9 @@ func (c *Config) setupLogging(appType AppType) error { // Currently the design is to close this from main() as a deferred // call. c.LogFileHandle = f - // TODO: Set c.LogFileHandle to the newly opened file - default: - // Explicitly note that we disable use of a log file for all - // other application types. - useLogFile = false + default: // Nagios doesn't look at stderr, only stdout. We have to make sure // that only whatever output is meant for consumption is emitted to @@ -154,20 +147,29 @@ func (c *Config) setupLogging(appType AppType) error { // If we're not setting up the configuration for the Nagios plugin, we // will attempt to use another output target. logOutput = os.Stderr - } - // We set some common fields here so that we don't have to repeat them - // explicitly later and then set additional fields while processing each - // email account. This approach is intended to help standardize the log - // messages to make them easier to search through later when - // troubleshooting. - c.Log = zerolog.New(logOutput).With().Caller(). - Str("version", Version()). - Bool("use_log_file", useLogFile). - Str("network_type", c.NetworkType). - Str("min_tls_version", c.MinTLSVersionKeyword()). - Logger() + switch { + case appType.InspectorIMAPCaps: + + // Slimline logger to emit messages in a format more appropriate to + // CLI "inspector" tool. + consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} + c.Log = zerolog.New(consoleWriter).With().Timestamp().Caller().Logger() + + default: + + // We set some common fields here so that we don't have to repeat them + // explicitly later and then set additional fields while processing + // each email account. This approach is intended to help standardize + // the log messages to make them easier to search through later when + // troubleshooting. + c.Log = zerolog.New(logOutput).With().Caller(). + Str("version", Version()). + Str("network_type", c.NetworkType). + Str("min_tls_version", c.MinTLSVersionKeyword()). + Logger() + } if err := setLoggingLevel(c.LoggingLevel); err != nil { return err diff --git a/internal/config/validate.go b/internal/config/validate.go index 8e240668..33a3e67b 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -49,9 +49,9 @@ func validateLoggingLevels(c Config) error { return nil } -func validateAccounts(c Config) error { +func validateAccounts(c Config, appType AppType) error { for _, account := range c.Accounts { - if account.Folders == nil { + if account.Folders == nil && !appType.InspectorIMAPCaps { return fmt.Errorf( "one or more folders not provided for account %s", account.Name, @@ -66,13 +66,15 @@ func validateAccounts(c Config) error { ) } - if account.Username == "" { + // Inspector app does not use this value. Other tools do. + if account.Username == "" && !appType.InspectorIMAPCaps { return fmt.Errorf("username not provided for account %s", account.Name, ) } - if account.Password == "" { + // Inspector app does not use this value. Other tools do. + if account.Password == "" && !appType.InspectorIMAPCaps { return fmt.Errorf("password not provided for account %s", account.Name, ) @@ -93,9 +95,29 @@ func validateAccounts(c Config) error { func (c Config) validate(appType AppType) error { switch { + case appType.InspectorIMAPCaps: + + // We're using the Accounts collection in order to obtain access to + // the server and port fields. + if err := validateAccounts(c, appType); err != nil { + return err + } + + if err := validateTLSVersion(c); err != nil { + return err + } + + if err := validateNetworkType(c); err != nil { + return err + } + + if err := validateLoggingLevels(c); err != nil { + return err + } + case appType.PluginIMAPMailboxBasicAuth: - if err := validateAccounts(c); err != nil { + if err := validateAccounts(c, appType); err != nil { return err } @@ -138,7 +160,7 @@ func (c Config) validate(appType AppType) error { return fmt.Errorf("missing log file output directory") } - if err := validateAccounts(c); err != nil { + if err := validateAccounts(c, appType); err != nil { return err } diff --git a/internal/mbxs/connect.go b/internal/mbxs/connect.go index 94e5e6ab..60b48535 100644 --- a/internal/mbxs/connect.go +++ b/internal/mbxs/connect.go @@ -66,10 +66,9 @@ func openConnection(addrs []string, port int, dialer Dialer, tlsConfig *tls.Conf } // If no connection errors were received, we can consider the - // connection attempt a success, clear any previous error and abort - // attempts to connect to any remaining IP Addresses for the specified - // server name. - logger.Info(). + // connection attempt a success and skip further attempts to connect + // to any remaining IP Addresses for the specified server name. + logger.Debug(). Str("ip_address", addr). Msg("Connected to server") @@ -299,7 +298,7 @@ func Login(c *client.Client, username string, password string, logger zerolog.Lo return fmt.Errorf("%s: %w", errMsg, err) } - logger.Info().Msg("Logged in") + logger.Debug().Msg("Logged in") return nil