feat: add doctor command
This commit is contained in:
parent
460868f4de
commit
2aa5e92b50
6 changed files with 392 additions and 4 deletions
19
README.md
19
README.md
|
|
@ -15,6 +15,7 @@ Le binaire s’appuie maintenant sur [`mcp-framework`](../mcp-framework) pour :
|
||||||
- `email-mcp config` : configure un profil IMAP
|
- `email-mcp config` : configure un profil IMAP
|
||||||
- `email-mcp setup` : alias de compatibilité vers `config`
|
- `email-mcp setup` : alias de compatibilité vers `config`
|
||||||
- `email-mcp mcp` : lance le serveur MCP sur `stdin/stdout`
|
- `email-mcp mcp` : lance le serveur MCP sur `stdin/stdout`
|
||||||
|
- `email-mcp doctor` : diagnostique la configuration locale, le wallet, le manifeste et l’accès IMAP
|
||||||
- `email-mcp update` : met à jour le binaire courant depuis la dernière release
|
- `email-mcp update` : met à jour le binaire courant depuis la dernière release
|
||||||
|
|
||||||
## Outils MCP
|
## Outils MCP
|
||||||
|
|
@ -92,6 +93,24 @@ base_url = "https://gitea.lclr.dev"
|
||||||
latest_release_url = "https://gitea.lclr.dev/api/v1/repos/AI/email-mcp/releases/latest"
|
latest_release_url = "https://gitea.lclr.dev/api/v1/repos/AI/email-mcp/releases/latest"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Diagnostic
|
||||||
|
|
||||||
|
`email-mcp doctor` vérifie :
|
||||||
|
|
||||||
|
- la lisibilité du fichier de configuration
|
||||||
|
- le profil IMAP résolu
|
||||||
|
- la disponibilité du wallet système
|
||||||
|
- la présence du mot de passe stocké
|
||||||
|
- la validité du manifeste `mcp.toml`
|
||||||
|
- la connectivité IMAP avec les credentials résolus
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./email-mcp doctor
|
||||||
|
./email-mcp doctor --profile work
|
||||||
|
```
|
||||||
|
|
||||||
|
La commande retourne un code de sortie non nul si au moins un check échoue.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Claude Code CLI
|
### Claude Code CLI
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module email-mcp
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.lclr.dev/AI/mcp-framework v1.1.0
|
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1
|
||||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
||||||
github.com/emersion/go-message v0.18.2
|
github.com/emersion/go-message v0.18.2
|
||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,5 +1,5 @@
|
||||||
gitea.lclr.dev/AI/mcp-framework v1.1.0 h1:/7fqiXmhir2mDxg3s1ReKrsnFNtSaBN7eQl5zXyYcdc=
|
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1 h1:S4YZ7G9b5RI7DmgTTEzWJKsWFf9Z1guPjxyajkcNLI0=
|
||||||
gitea.lclr.dev/AI/mcp-framework v1.1.0/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4=
|
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4=
|
||||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
||||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
||||||
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ func NewAppWithDependencies(
|
||||||
|
|
||||||
func (a *App) Run(args []string) error {
|
func (a *App) Run(args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("usage: email-mcp <config|setup|mcp|update>")
|
return fmt.Errorf("usage: email-mcp <config|setup|mcp|doctor|update>")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
|
|
@ -121,6 +121,8 @@ func (a *App) Run(args []string) error {
|
||||||
return a.runConfig(context.Background(), args[0], args[1:])
|
return a.runConfig(context.Background(), args[0], args[1:])
|
||||||
case "mcp":
|
case "mcp":
|
||||||
return a.runMCP(context.Background(), args[1:])
|
return a.runMCP(context.Background(), args[1:])
|
||||||
|
case "doctor":
|
||||||
|
return a.runDoctor(context.Background(), args[1:])
|
||||||
case "update":
|
case "update":
|
||||||
return a.runUpdate(context.Background(), args[1:])
|
return a.runUpdate(context.Background(), args[1:])
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,9 @@ func TestAppRunShowsUsageWhenNoArgsProvided(t *testing.T) {
|
||||||
if !strings.Contains(err.Error(), "usage:") {
|
if !strings.Contains(err.Error(), "usage:") {
|
||||||
t.Fatalf("expected usage text in error, got %q", err.Error())
|
t.Fatalf("expected usage text in error, got %q", err.Error())
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(err.Error(), "doctor") {
|
||||||
|
t.Fatalf("expected usage to mention doctor, got %q", err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppRunConfigPromptsAndSavesProfile(t *testing.T) {
|
func TestAppRunConfigPromptsAndSavesProfile(t *testing.T) {
|
||||||
|
|
@ -383,6 +386,158 @@ latest_release_url = "http://127.0.0.1:1/releases/latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type doctorMailServiceStub struct {
|
||||||
|
listMailboxes []imapclient.Mailbox
|
||||||
|
listErr error
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *doctorMailServiceStub) ListMailboxes(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) {
|
||||||
|
s.called = true
|
||||||
|
return s.listMailboxes, s.listErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *doctorMailServiceStub) ListMessages(context.Context, secretstore.Credential, string, int) ([]imapclient.MessageSummary, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *doctorMailServiceStub) GetMessage(context.Context, secretstore.Credential, string, uint32) (imapclient.Message, error) {
|
||||||
|
return imapclient.Message{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppRunDoctorRendersReportAndChecksConnectivity(t *testing.T) {
|
||||||
|
tempHome := t.TempDir()
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", tempHome)
|
||||||
|
t.Setenv("HOME", tempHome)
|
||||||
|
|
||||||
|
store := frameworkconfig.NewStore[ProfileConfig](binaryName)
|
||||||
|
configPath, err := store.ConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ConfigPath returned error: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.Save(configPath, frameworkconfig.FileConfig[ProfileConfig]{
|
||||||
|
Version: frameworkconfig.CurrentVersion,
|
||||||
|
CurrentProfile: "work",
|
||||||
|
Profiles: map[string]ProfileConfig{
|
||||||
|
"work": {
|
||||||
|
Host: "imap.example.com",
|
||||||
|
Username: "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Save returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(manifestDir, "mcp.toml"), []byte(`
|
||||||
|
[update]
|
||||||
|
latest_release_url = "https://example.com/releases/latest"
|
||||||
|
`), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets := &secretStoreStub{
|
||||||
|
values: map[string]string{
|
||||||
|
"imap-password/work": "secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mail := &doctorMailServiceStub{
|
||||||
|
listMailboxes: []imapclient.Mailbox{{Name: "INBOX"}},
|
||||||
|
}
|
||||||
|
output := &bytes.Buffer{}
|
||||||
|
|
||||||
|
app := NewAppWithDependencies(
|
||||||
|
nil,
|
||||||
|
store,
|
||||||
|
func() (secretStore, error) { return secrets, nil },
|
||||||
|
func() mcpserver.MailService { return mail },
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
func() (string, error) { return filepath.Join(manifestDir, "email-mcp"), nil },
|
||||||
|
nil,
|
||||||
|
output,
|
||||||
|
&bytes.Buffer{},
|
||||||
|
"dev",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := app.Run([]string{"doctor"}); err != nil {
|
||||||
|
t.Fatalf("doctor returned error: %v", err)
|
||||||
|
}
|
||||||
|
if !mail.called {
|
||||||
|
t.Fatal("expected connectivity check to call mail service")
|
||||||
|
}
|
||||||
|
|
||||||
|
text := output.String()
|
||||||
|
for _, needle := range []string{
|
||||||
|
"[OK] config: config file is readable",
|
||||||
|
"[OK] profile: resolved profile is complete",
|
||||||
|
"[OK] password: stored password is present",
|
||||||
|
"[OK] connectivity: IMAP server is reachable",
|
||||||
|
"Summary: 6 ok, 0 warning(s), 0 failure(s), 6 total",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(text, needle) {
|
||||||
|
t.Fatalf("output = %q, want substring %q", text, needle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppRunDoctorReturnsErrorWhenChecksFail(t *testing.T) {
|
||||||
|
tempHome := t.TempDir()
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", tempHome)
|
||||||
|
t.Setenv("HOME", tempHome)
|
||||||
|
|
||||||
|
store := frameworkconfig.NewStore[ProfileConfig](binaryName)
|
||||||
|
configPath, err := store.ConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ConfigPath returned error: %v", err)
|
||||||
|
}
|
||||||
|
if err := store.Save(configPath, frameworkconfig.FileConfig[ProfileConfig]{
|
||||||
|
Version: frameworkconfig.CurrentVersion,
|
||||||
|
Profiles: map[string]ProfileConfig{
|
||||||
|
"default": {
|
||||||
|
Host: "imap.example.com",
|
||||||
|
Username: "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Save returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(manifestDir, "mcp.toml"), []byte(`
|
||||||
|
[update]
|
||||||
|
latest_release_url = "https://example.com/releases/latest"
|
||||||
|
`), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := &bytes.Buffer{}
|
||||||
|
app := NewAppWithDependencies(
|
||||||
|
nil,
|
||||||
|
store,
|
||||||
|
func() (secretStore, error) { return &secretStoreStub{}, nil },
|
||||||
|
func() mcpserver.MailService { return &doctorMailServiceStub{} },
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
func() (string, error) { return filepath.Join(manifestDir, "email-mcp"), nil },
|
||||||
|
nil,
|
||||||
|
output,
|
||||||
|
&bytes.Buffer{},
|
||||||
|
"dev",
|
||||||
|
)
|
||||||
|
|
||||||
|
err = app.Run([]string{"doctor"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected doctor to fail when password is missing")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "doctor checks failed") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output.String(), "[FAIL] password: stored password is missing") {
|
||||||
|
t.Fatalf("unexpected output: %q", output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAppRunReturnsClearErrorsWhenDependenciesMissing(t *testing.T) {
|
func TestAppRunReturnsClearErrorsWhenDependenciesMissing(t *testing.T) {
|
||||||
app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, nil, &bytes.Buffer{}, "dev")
|
app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, nil, &bytes.Buffer{}, "dev")
|
||||||
|
|
||||||
|
|
@ -392,6 +547,7 @@ func TestAppRunReturnsClearErrorsWhenDependenciesMissing(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{command: "config", want: "config prompter is not configured"},
|
{command: "config", want: "config prompter is not configured"},
|
||||||
{command: "mcp", want: "mcp runner is not configured"},
|
{command: "mcp", want: "mcp runner is not configured"},
|
||||||
|
{command: "doctor", want: "config store is not configured"},
|
||||||
{command: "update", want: "manifest loader is not configured"},
|
{command: "update", want: "manifest loader is not configured"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
211
internal/cli/doctor.go
Normal file
211
internal/cli/doctor.go
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
||||||
|
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
||||||
|
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
||||||
|
frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) runDoctor(ctx context.Context, args []string) error {
|
||||||
|
profileFlag, err := parseProfileArgs("doctor", args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a.configStore == nil {
|
||||||
|
return fmt.Errorf("config store is not configured")
|
||||||
|
}
|
||||||
|
if a.openSecretStore == nil {
|
||||||
|
return fmt.Errorf("secret store is not configured")
|
||||||
|
}
|
||||||
|
if a.newMailService == nil {
|
||||||
|
return fmt.Errorf("mail service is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
report := frameworkcli.RunDoctor(ctx, frameworkcli.DoctorOptions{
|
||||||
|
ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](binaryName)),
|
||||||
|
SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.frameworkSecretStoreFactory()),
|
||||||
|
ManifestDir: a.doctorManifestDir(),
|
||||||
|
ManifestValidator: func(file frameworkmanifest.File, _ string) []string {
|
||||||
|
if strings.TrimSpace(file.Update.LatestReleaseURL) == "" {
|
||||||
|
return []string{"update.latest_release_url must not be empty"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConnectivityCheck: a.doctorConnectivityCheck(profileFlag),
|
||||||
|
ExtraChecks: []frameworkcli.DoctorCheck{
|
||||||
|
a.doctorProfileCheck(profileFlag),
|
||||||
|
a.doctorPasswordCheck(profileFlag),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err := frameworkcli.RenderDoctorReport(a.stdout, report); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if report.HasFailures() {
|
||||||
|
return fmt.Errorf("doctor checks failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) frameworkSecretStoreFactory() func() (frameworksecretstore.Store, error) {
|
||||||
|
return func() (frameworksecretstore.Store, error) {
|
||||||
|
store, err := a.openSecretStore()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) doctorManifestDir() string {
|
||||||
|
if a.resolveExecutable == nil {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
executablePath, err := a.resolveExecutable()
|
||||||
|
if err != nil {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return filepath.Dir(executablePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) doctorProfileCheck(profileFlag string) frameworkcli.DoctorCheck {
|
||||||
|
return func(context.Context) frameworkcli.DoctorResult {
|
||||||
|
cfg, _, err := a.configStore.LoadDefault()
|
||||||
|
if err != nil {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "profile",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "cannot load profile configuration",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileName := frameworkcli.ResolveProfileName(profileFlag, os.Getenv(defaultProfileEnv), cfg.CurrentProfile)
|
||||||
|
profile, ok := cfg.Profiles[profileName]
|
||||||
|
if !ok {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "profile",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "resolved profile is missing",
|
||||||
|
Detail: fmt.Sprintf("profile %q", profileName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var issues []string
|
||||||
|
if strings.TrimSpace(profile.Host) == "" {
|
||||||
|
issues = append(issues, "host is empty")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(profile.Username) == "" {
|
||||||
|
issues = append(issues, "username is empty")
|
||||||
|
}
|
||||||
|
if len(issues) > 0 {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "profile",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "resolved profile is incomplete",
|
||||||
|
Detail: fmt.Sprintf("profile %q: %s", profileName, strings.Join(issues, "; ")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "profile",
|
||||||
|
Status: frameworkcli.DoctorStatusOK,
|
||||||
|
Summary: "resolved profile is complete",
|
||||||
|
Detail: fmt.Sprintf("profile %q", profileName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) doctorPasswordCheck(profileFlag string) frameworkcli.DoctorCheck {
|
||||||
|
return func(context.Context) frameworkcli.DoctorResult {
|
||||||
|
profileName := a.resolveDoctorProfileName(profileFlag)
|
||||||
|
store, err := a.openSecretStore()
|
||||||
|
if err != nil {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "password",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "cannot inspect stored password",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hasPassword, err := loadStoredPassword(store, profileName)
|
||||||
|
if err != nil {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "password",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "cannot read stored password",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasPassword {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "password",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "stored password is missing",
|
||||||
|
Detail: fmt.Sprintf("secret %q", passwordSecretName(profileName)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "password",
|
||||||
|
Status: frameworkcli.DoctorStatusOK,
|
||||||
|
Summary: "stored password is present",
|
||||||
|
Detail: fmt.Sprintf("secret %q", passwordSecretName(profileName)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) doctorConnectivityCheck(profileFlag string) frameworkcli.DoctorCheck {
|
||||||
|
return func(parent context.Context) frameworkcli.DoctorResult {
|
||||||
|
ctx, cancel := context.WithTimeout(parent, 35*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cred, err := a.loadCredential(profileFlag)
|
||||||
|
if err != nil {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "connectivity",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "cannot load IMAP credentials",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := a.newMailService().ListMailboxes(ctx, cred); err != nil {
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "connectivity",
|
||||||
|
Status: frameworkcli.DoctorStatusFail,
|
||||||
|
Summary: "IMAP server is unreachable or rejected authentication",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frameworkcli.DoctorResult{
|
||||||
|
Name: "connectivity",
|
||||||
|
Status: frameworkcli.DoctorStatusOK,
|
||||||
|
Summary: "IMAP server is reachable",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) resolveDoctorProfileName(profileFlag string) string {
|
||||||
|
envProfile := os.Getenv(defaultProfileEnv)
|
||||||
|
if a.configStore == nil {
|
||||||
|
return frameworkcli.ResolveProfileName(profileFlag, envProfile, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, _, err := a.configStore.LoadDefault()
|
||||||
|
if err != nil {
|
||||||
|
return frameworkcli.ResolveProfileName(profileFlag, envProfile, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return frameworkcli.ResolveProfileName(profileFlag, envProfile, cfg.CurrentProfile)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue