Skip to content

Latest commit

 

History

276 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🐺 OkamiUNI

Email e agenda, juntos, nativos, do jeito que o macOS merece.

Swift 6.3 macOS 26 SwiftUI Testes Mutações Temas Marco

A janela principal do OkamiUNI: barra lateral com contas e caixas, lista de mensagens, leitor com resumo no dispositivo e trilha de agenda do dia.

TL;DR

  • 📬 Um app só para email e agenda — a trilha do dia mora ao lado do leitor, e selecionar uma caixa filtra as duas.
  • 🔄 As contas se mantêm sozinhas: IMAP IDLE, Gmail incremental por histórico, e a rede que volta acorda tudo. Ação feita offline chega ao servidor quando a conexão volta — com fila transacional, retry e "tentar de novo" explicado.
  • 📖 Leitor de verdade: HTML renderizado (JavaScript morto, imagem remota bloqueada por padrão com memória de confiança por remetente), conversas agrupadas em pilha, convite de agenda vira cartão com "Colocar na agenda" — sem duplicar, por UID.
  • ✉️ Enviar envia: Gmail pela API, qualquer IMAP por SMTP próprio (STARTTLS antes da senha), caixa Enviadas, resposta com In-Reply-To, idempotência por Message-ID — timeout ambíguo nunca duplica email.
  • 🗂️ As pastas do provedor na barra lateral, expansíveis por conta — e um arquivar que cria a pasta que falta no servidor em vez de parar a fila.
  • 🖱️ Ações onde a mão espera: botão direito custom em toda superfície, arraste lateral com Desfazer, atalhos de verdade (⌘R ⇧⌘R ⇧⌘F ⌘E ⇧⌘L ⇧⌘U ⌘N ⌘K).
  • 🎨 26 temas, hairlines de 1 pixel em telas 1×, semáforos a 22pt verificados por ensaio — o polimento é requisito, não acabamento.
  • 🔌 Qualquer provedor: nada no código limita provedor, domínio, número de contas ou de pastas.
  • 1725 testes que provam por mutação: cada teste novo só conta depois de falhar com o defeito reintroduzido.
Tools/rodar.sh     # mata a instância antiga, regenera o projeto, compila e abre

Por que existe

Cliente de email é o app que mais horas passa aberto — e o que menos respeito costuma receber: web view, ações escondidas, agenda em outro app. O OkamiUNI nasce do desenho (design/, a fonte da verdade deste repositório) para o binário nativo, com uma regra que atravessa tudo: controle que existe faz alguma coisa — ou aparece desabilitado explicando por quê. Botão mudo é defeito, não estado.

O que já funciona

Marco 1 — o shell

Área O que tem
📥 Caixa de entrada Fluxo de triagem (Hoje · Depois · Tudo · Arquivado · Lixeira · Enviadas), busca que dobra acento ("Revisao" acha "Revisão"), filtro por conta que alcança lista e agenda, ponto + fundo de não-lida, estrela de sinalizada, data honesta na linha (hoje → hora · "Ontem" · "21 de jul.")
↔️ Arraste na linha Duas ações por lado, persistidas e configuráveis; a linha para aberta, o disparo longo inunda de cor antes de executar, destrutivo tem Desfazer
🖱️ Botão direito Menu custom no idioma do design em 10 superfícies — responder, responder a todos, encaminhar, arquivar, apagar, sinalizar, mover, copiar; submenu, atalhos exibidos, navegação por teclado (↑↓⏎→← Esc)
⌨️ Atalhos Menu Mensagem na barra do sistema; ⌘R responder · ⇧⌘R responder a todos · ⇧⌘F encaminhar · ⌘E arquivar · apagar · ⇧⌘L sinalizar · ⇧⌘U lida/não lida · ⌘N nova · ⌘K busca — e campo de texto nunca perde tecla sem modificador
✍️ Composer NSTextView de verdade: formatação na seleção, tabelas (Enter não quebra), hyperlink, justificado, cor livre, fontes do sistema, assinatura por conta; a faixa de resposta rápida nasce recolhida e o rascunho sobrevive ao recolher
🪟 Janelas Composer, nova mensagem, mensagem destacada e detalhe de compromisso são cenas reais (⌘W, menu Janela, uma por valor) — todas com o cabeçalho na linha do semáforo, verificado por ensaio
🎨 Shell 26 temas com tokens de ponta a ponta, hairlines de 1 pixel de dispositivo, duplo clique na barra respeitando a preferência do sistema, painéis redimensionáveis com intenção preservada

Marco 2 — contas de verdade

Área O que tem
🔐 OAuth do Google PKCE S256 validado contra o vetor oficial do RFC 7636, redirect derivado do próprio client ID, refresh com corrida única por conta, client_secret de app desktop
📡 IMAP para qualquer provedor Cliente próprio sobre SwiftNIO com framing de literais, STARTTLS obrigatório antes de qualquer credencial, detecção de servidor por endereço, toda espera com teto e cancelamento
🔑 Segredos Só no Keychain — nunca em banco, log ou arquivo. Assinatura estável do binário: o Keychain pede a senha uma vez, não a cada build
💾 Local-first SQLite (GRDB) com FTS5 de acento dobrado no corpo, carga retomável dos últimos 90 dias — o app abre offline. Sem conta conectada, ele continua sendo o shell do Marco 1, com as fixtures

Marco 3 — sincronização de verdade

Área O que tem
🔄 Sync contínuo IMAP IDLE (reengate ≤25min, DONE que sai sempre) com delta de chegadas/bandeiras/expurgos; Gmail incremental por history.list com recarga idempotente quando o marcador expira; NWPathMonitor acorda sync e fila quando a rede volta
📤 Fila espelhada Toda ação (arquivar, apagar, lida, estrela, mover, enviar) persiste e enfileira na mesma transação; executor por conta com claim atômico, backoff, e idempotência por UUID. Falha permanente para com a causa na tela e "Tentar de novo" — que religa de verdade. Fila parada sobrevive ao reinício e não executa nada fora de ordem
📖 Leitor rico HTML sanitizado (script/iframe/form fora, cid: embutido, anexo inline do Gmail buscado pela API) numa WebView travada: JS morto, zero requisição remota por padrão, links no navegador. Imagem remota com "Carregar" por mensagem e "Sempre carregar deste remetente" (endereço exato, revogável). Email de largura fixa encolhe para caber; height:100% de marketing não colapsa; espera tem roda + texto plano legível por baixo, e voltar à mensagem é instantâneo (acervo de sessão, nada em disco)
🧵 Conversas Agrupadas por threadId (Gmail) / corrente de References (IMAP) / assunto normalizado (fallback), uma linha por conversa com contagem, pilha cronológica no leitor (anteriores recolhidas), ações da linha alcançam a conversa inteira — e a resposta enviada carrega In-Reply-To
✉️ Envio RFC 5322 de verdade (RFC 2047 no assunto, multipart texto+HTML, Message-ID próprio); Gmail pela API, IMAP por SMTP (EHLO→STARTTLS→AUTH, dot-stuffing) + APPEND em Enviadas; pela fila: offline funciona, greylisting re-tenta, endereço recusado explica, timeout ambíguo checa antes de reenviar
🗂️ Pastas do provedor LIST com special-use (RFC 6154) e labels do Gmail na barra lateral, expansíveis por conta, não-lidas por pasta; destino de move que não existe é criado no servidor e a operação repete
📅 Agenda que lembra Convite (text/calendar) vira cartão com organizador, participantes, local limpo, link da reunião e "Colocar na agenda" — dedup por UID (50 encaminhamentos = 1 evento), "Convite atualizado" atualiza. Compromisso criado sobrevive ao reinício; "Entrar" abre a reunião; a mensagem de origem se lê dentro do compromisso
👥 Contatos reais O autocomplete sugere quem troca email com as contas conectadas, por frequência e recência — as fixtures só ficam para quem não conectou nada
Linha arrastada descansando aberta com as ações Arquivar e Lida reveladas.
O arraste para aberto…
Arraste longo com o painel inteiro inundado pela cor de destaque, pronto para disparar.
…e inunda antes de disparar.

Como rodar

Pré-requisitos: Xcode 26.6+ (Swift 6.3) e XcodeGen (brew install xcodegen).

git clone https://github.com/OkamiOps/okamiuni.git
cd okamiuni
Tools/rodar.sh

O script encerra a instância anterior, limpa o estado salvo da janela, regenera o .xcodeproj, compila, imprime a data do binário e o commit, e abre o app.

Para a rota Google, copie Config/Google.example.xcconfig para Config/Google.xcconfig (gitignored) e cole o seu client ID de app desktop — o roteiro completo está em docs/oauth-google.md. Qualquer outro provedor entra por IMAP com senha de app (docs/senha-de-app.md).

Arquitetura

Quatro pacotes Swift e um princípio: lógica pura fora das views — uma View SwiftUI é @MainActor implícito, e tudo que merece teste nonisolated mora em UNICore.

Pacote Papel Exemplos
Packages/UNICore Modelo e lógica pura, sem SwiftUI MailStore, ConversationStack, ICalendar, PlainTextReflow, ThreadKey, ContextMenus, WeekAgenda/MonthAgenda
Packages/UNIDesign O sistema de temas — 26 temas, tokens de cor, tipografia, fontes embarcadas Theme, ThemeStore, FontRegistry
Packages/UNIShell As telas e o chrome da janela InboxScreen, ReaderPane, CalendarScreen, ComposerWindow, WindowChrome, os menus custom
Packages/UNISync Contas e sincronização AccountDirector, GoogleAuth, ImapSession, SmtpSession, OutboxExecutor, SyncRunner, MimeBody, SyncDatabase
App/  ──▶  UNIShell  ──▶  UNIDesign
                └───────▶  UNICore  ◀──  UNISync  ──▶  (Keychain · GRDB · Gmail API · IMAP · SMTP)

O projeto Xcode é gerado por project.yml (XcodeGen) com SWIFT_STRICT_CONCURRENCY: complete. O desenho original — HTML navegável — vive em design/ e é tratado como especificação: quando uma medida está em dúvida, o protótipo é servido e medido, não lido.

Como este projeto se testa

Swift Testing (nunca XCTest), 1725 testes em quatro pacotes — e uma regra que virou cultura: teste que passa com o código quebrado é defeito. Todo teste novo nasce provado vermelho com o defeito reintroduzido; mais de 190 mutações registradas mataram, entre outras, um quoted-printable que comia a última letra de cada linha, uma fila que engolia a terceira ação de um ciclo ler→não ler→ler, e um "esvaziar a lixeira" que só funcionava uma vez na vida da conta.

Cinco instrumentos fazem o app testemunhar contra si mesmo, sem tocar no mouse de ninguém:

Instrumento Bandeira O que faz
Captura --capturar A janela real se fotografa e encerra — pixels do AppKit, não de um harness
Ensaio de arraste --ensaiar-arraste Eventos de mouse sintetizados dentro do processo (NSWindow.sendEvent), uma foto por fase do gesto
Ensaio de teclado / barra --ensaiar-teclado · --ensaiar-barra Cada atalho e o duplo clique na barra, aferidos no caminho real dos eventos
Ensaio de contas --ensaiar-contas O fluxo inteiro de conectar uma conta, contra um servidor IMAP falso em loopback — banco descartável, Keychain intocado
Ensaio de semáforos --ensaiar-semaforos Abre as seis janelas, lê a moldura real dos botões do sistema e mede o alinhamento do cabeçalho — 6 janelas, diferença 0.0, verificado

Nenhum teste toca rede externa: IMAP e SMTP falam com servidores falsos em 127.0.0.1, o Gmail com um transport stub, e até a imagem remota lenta dos testes do leitor sai de um servidor local que conta requisições. Foi assim que se provou que voltar a uma mensagem custa zero downloads.

for p in UNICore UNIDesign UNIShell UNISync; do (cd "Packages/$p" && swift test); done

O registro das decisões — por que o Button do macOS dispara no mouse-up depois de 200pt de arraste, por que NSApp.postEvent mata um processo de teste em silêncio, por que !important de folha perde para !important inline — está em docs/decisoes-de-engenharia.md.

Roteiro

  • Marco 1 — Shell: o app inteiro navegável, com as quatro contas vindo de fixtures
  • Marco 2 — Contas: OAuth do Google (PKCE), IMAP para qualquer provedor, Keychain, banco SQLite local-first com FTS5, carga de 90 dias retomável
  • Marco 3 — Sincronização: sync contínuo (IDLE + histórico), fila de ações espelhada com autocura, leitor HTML seguro, conversas, envio (API + SMTP), Enviadas, pastas do provedor, convites com dedup, contatos reais
  • Marco 4 — Agenda real: EventKit/CalDAV; RSVP do convite (iTIP); anexos
  • Marco 5 — Inteligência no dispositivo: resumo e detecção de compromisso deixando as fixtures

Dívidas deliberadas, registradas onde doem: anexos visíveis, RSVP (exige METHOD:REPLY por SMTP), recorrência de evento, árvore de pastas indentada (o delimitador ainda não sobe pelo fio), encaminhar convite com o .ics junto.

Princípios de engenharia

  1. O design é a especificação. O HTML em design/ decide medida, cor e comportamento; divergência é bug com número dos dois lados.
  2. Nenhum controle mudo. Faz, ou explica por que não pode — o "Entrar" da reunião, o "Tentar de novo" da fila e o "Sempre carregar" do remetente existem porque botão que finge é defeito.
  3. Nada limita contas. Provedor, domínio, quantidade de contas e de pastas são ilimitados por construção.
  4. Fuso horário não atravessa o modelo. Horário é minuto-do-dia, dia é data civil — Date só nas bordas.
  5. Prova no app real. Conserto de interação só conta com ensaio antes e depois, no caminho real dos eventos — inclusive a moldura dos botões que o bitmap não vê.
  6. Privacidade por padrão. Imagem remota é rastreador: bloqueada até você mandar, confiança por endereço exato, cache só em memória, segredos só no Keychain.
  7. Telas 1× importam. Meia unidade de ponto é zero ou um pixel; hairline é 1/displayScale, borda é strokeBorder, nunca .stroke fino.

Feito com Swift, teimosia e um desenho que veio primeiro. 🐺

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages