this repo has no description smallweb.run
smallweb
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

add email support

pomdtr 23be1d8f 7a65ffa2

+113 -4
+2 -1
.vscode/launch.json
··· 8 8 "args": [ 9 9 "up", 10 10 "--addr=:7777", 11 - "--ssh-addr=:2222" 11 + "--ssh-addr=:2222", 12 + "--smtp-addr=:2525", 12 13 ], 13 14 "env": { 14 15 "SMALLWEB_DIR": "${workspaceFolder}/example",
+33 -3
cmd/up.go
··· 29 29 "github.com/coreos/go-oidc/v3/oidc" 30 30 "github.com/creack/pty" 31 31 "github.com/knadh/koanf/providers/file" 32 + "github.com/mhale/smtpd" 32 33 33 34 "github.com/knadh/koanf/providers/posflag" 34 35 "github.com/knadh/koanf/v2" ··· 50 51 addr string 51 52 apiAddr string 52 53 sshAddr string 54 + smtpAddr string 53 55 sshPrivateKey string 54 56 tlsCert string 55 57 tlsKey string ··· 201 203 go http.Serve(ln, mux) 202 204 } 203 205 206 + if flags.smtpAddr != "" { 207 + fmt.Fprintf(cmd.ErrOrStderr(), "Starting smtp server on %s...\n", flags.smtpAddr) 208 + go smtpd.ListenAndServe(flags.smtpAddr, func(remoteAddr net.Addr, from string, to []string, msg []byte) error { 209 + for _, recipient := range to { 210 + parts := strings.Split(recipient, "@") 211 + if len(parts) != 2 { 212 + return fmt.Errorf("invalid recipient: %s", recipient) 213 + } 214 + 215 + account, domain := parts[0], parts[1] 216 + if domain != k.String("domain") { 217 + return fmt.Errorf("invalid domain: %s", domain) 218 + } 219 + 220 + a, err := app.LoadApp(account, k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", parts[0]))) 221 + if err != nil { 222 + return fmt.Errorf("failed to load app: %v", err) 223 + } 224 + 225 + worker := worker.NewWorker(a) 226 + if err := worker.SendEmail(context.Background(), msg); err != nil { 227 + return fmt.Errorf("failed to send email: %v", err) 228 + } 229 + } 230 + 231 + return nil 232 + }, "smallweb", k.String("domain")) 233 + } 234 + 204 235 if flags.sshAddr != "" { 205 236 sshPrivateKeyPath := flags.sshPrivateKey 206 237 if flags.sshPrivateKey == "" { ··· 370 401 } 371 402 372 403 fmt.Fprintf(cmd.ErrOrStderr(), "Starting ssh server on %s...\n", flags.sshAddr) 373 - if err = srv.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 374 - fmt.Fprintf(cmd.ErrOrStderr(), "failed to start ssh server: %v\n", err) 375 - } 404 + go srv.ListenAndServe() 376 405 } 377 406 378 407 // sigint handling ··· 386 415 387 416 cmd.Flags().StringVar(&flags.addr, "addr", "", "address to listen on") 388 417 cmd.Flags().StringVar(&flags.sshAddr, "ssh-addr", "", "address to listen on for ssh/sftp") 418 + cmd.Flags().StringVar(&flags.smtpAddr, "smtp-addr", "", "address to listen on for smtp") 389 419 cmd.Flags().StringVar(&flags.sshPrivateKey, "ssh-private-key", "", "ssh private key") 390 420 cmd.Flags().StringVar(&flags.sshPrivateKey, "ssh-host-key", "", "ssh host key") 391 421 cmd.Flags().StringVar(&flags.tlsCert, "tls-cert", "", "tls certificate file")
+5
example/email/email.txt
··· 1 + From: sender@example.com 2 + To: email@smallweb.localhost 3 + Subject: Test Email 4 + 5 + This is a test email sent via curl.
+11
example/email/main.ts
··· 1 + import { ensureDir } from "jsr:@std/fs@^1.0.15/ensure-dir"; 2 + import PostalMime from "npm:postal-mime@2.4.3" 3 + 4 + export default { 5 + async email(msg: Uint8Array) { 6 + await ensureDir("./data") 7 + 8 + const email = await PostalMime.parse(msg); 9 + await Deno.writeTextFile(`./data/email.json`, JSON.stringify(email, null, 2)); 10 + } 11 + }
+6
example/email/send.sh
··· 1 + #!/bin/sh 2 + 3 + curl --url "smtp://localhost:2525" \ 4 + --mail-from "sender@example.com" \ 5 + --mail-rcpt "email@smallweb.localhost" \ 6 + --upload-file email.txt
+1
go.mod
··· 138 138 github.com/mattn/go-colorable v0.1.14 // indirect 139 139 github.com/mattn/go-localereader v0.0.1 // indirect 140 140 github.com/mattn/go-runewidth v0.0.16 // indirect 141 + github.com/mhale/smtpd v0.8.3 // indirect 141 142 github.com/mholt/acmez/v3 v3.1.1 // indirect 142 143 github.com/miekg/dns v1.1.63 // indirect 143 144 github.com/mitchellh/copystructure v1.2.0 // indirect
+2
go.sum
··· 308 308 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 309 309 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 310 310 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 311 + github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8= 312 + github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4= 311 313 github.com/mholt/acmez/v3 v3.1.1 h1:Jh+9uKHkPxUJdxM16q5mOr+G2V0aqkuFtNA28ihCxhQ= 312 314 github.com/mholt/acmez/v3 v3.1.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= 313 315 github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
+18
worker/sandbox.ts
··· 1 1 import { accepts } from "jsr:@std/http@1.0.12/negotiation" 2 2 import { escape } from "jsr:@std/html@1.0.3" 3 + import { decodeBase64 } from "jsr:@std/encoding@1.0.8/base64" 3 4 4 5 function cleanStack(str?: string) { 5 6 if (!str) return undefined; ··· 160 161 } 161 162 162 163 await handler.run(payload.args) 164 + } else if (payload.command === "email") { 165 + const mod = await import(payload.entrypoint); 166 + if (!mod.default || typeof mod.default !== "object") { 167 + console.error( 168 + "The mod does not provide an object as it's default export.", 169 + ); 170 + Deno.exit(1); 171 + } 172 + 173 + const handler = mod.default; 174 + if (!("email" in handler)) { 175 + console.error("The mod default export does not have a email function."); 176 + Deno.exit(1); 177 + } 178 + 179 + const msg = decodeBase64(payload.msg) 180 + await handler.email(msg) 163 181 } else { 164 182 console.error("Unknown command: ", payload.command); 165 183 Deno.exit(1);
+35
worker/worker.go
··· 4 4 "bufio" 5 5 "context" 6 6 _ "embed" 7 + "encoding/base64" 7 8 "encoding/json" 8 9 "fmt" 9 10 "io" ··· 493 494 command.Env = commandEnv(me.App) 494 495 495 496 return command, nil 497 + } 498 + 499 + func (me *Worker) SendEmail(ctx context.Context, data []byte) error { 500 + deno, err := DenoExecutable() 501 + if err != nil { 502 + return fmt.Errorf("could not find deno executable") 503 + } 504 + 505 + denoArgs := []string{"run"} 506 + denoArgs = append(denoArgs, me.DenoArgs(deno, SandboxMethodRun)...) 507 + 508 + payload := strings.Builder{} 509 + encoder := json.NewEncoder(&payload) 510 + encoder.SetEscapeHTML(false) 511 + if err := encoder.Encode(map[string]any{ 512 + "command": "email", 513 + "entrypoint": me.App.Entrypoint(), 514 + "msg": base64.StdEncoding.EncodeToString(data), 515 + }); err != nil { 516 + return fmt.Errorf("could not encode input: %w", err) 517 + } 518 + 519 + denoArgs = append(denoArgs, "-", payload.String()) 520 + 521 + command := exec.CommandContext(ctx, deno, denoArgs...) 522 + 523 + command.Stdin = strings.NewReader(sandboxContent) 524 + command.Stderr = os.Stderr 525 + command.Stdout = os.Stdout 526 + command.Dir = me.App.Dir() 527 + 528 + command.Env = commandEnv(me.App) 529 + 530 + return command.Run() 496 531 } 497 532 498 533 // GetFreePort asks the kernel for a free open port that is ready to use.