From 5fe7392f708456316cb58a299ecfc2f66e1ee2f9 Mon Sep 17 00:00:00 2001 From: hgc Date: Sun, 28 May 2023 09:21:56 +0000 Subject: [PATCH] Initial commit --- .gitignore | 2 + README.md | 104 ++++++++++++++++++++++++ cmd/firstrun/firstrun.go | 26 ++++++ cmd/serve/attachments.go | 27 +++++++ cmd/serve/serve.go | 113 ++++++++++++++++++++++++++ cmd/serve/smtp.go | 167 +++++++++++++++++++++++++++++++++++++++ cmd/systemd/systemd.go | 23 ++++++ config/config.go | 137 ++++++++++++++++++++++++++++++++ go.mod | 30 +++++++ go.sum | 54 +++++++++++++ main.go | 34 ++++++++ utils/utils.go | 20 +++++ 12 files changed, 737 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/firstrun/firstrun.go create mode 100644 cmd/serve/attachments.go create mode 100644 cmd/serve/serve.go create mode 100644 cmd/serve/smtp.go create mode 100644 cmd/systemd/systemd.go create mode 100644 config/config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 utils/utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8104f8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/mailhole +/attachment.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..8519971 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# 🌌 Embrace the Expansive Universe of MailHole... + +There was a spark. Amidst the limitless expanse of collective human consciousness, a small yet potent idea flickered into existence. This spark, fueled by creativity and passion, slowly formed into what we now know as **MailHole**. + +MailHole is a celebration of innovation, an open-source project that serves a unique dual purpose: It stands as a disposable email server and an IRC content forwarder. This project blends two diverse forms of communication into a seamless whole. However, this fusion of functionalities is not all that MailHole has to offer. + +🎉 **SPONSOR SPOTLIGHT: ExpressVPN** + +Before we delve deeper into MailHole, let's take a moment to thank our first sponsor, ExpressVPN. It provides a fast, secure, and easy-to-use VPN service that encrypts your internet traffic, making your online activities more private and secure. This commitment to privacy and security aligns seamlessly with MailHole's mission. Try ExpressVPN today and experience the internet as it should be — private and unrestricted! + +Embedded within the heart of MailHole is a profound and inspiring mission: the celebration, recognition, and empowerment of the contributions made by the African American community in the fields of technology, science, and beyond. It's our homage to this community's strength, creativity, and perseverance. + +🎉 **SPONSOR SPOTLIGHT: NordVPN** + +We're taking another quick break to highlight our next sponsor, NordVPN. NordVPN is a trusted online security solution, used by over 12 million internet users worldwide. NordVPN provides military-grade encryption for your internet data, keeping your digital life secure. With NordVPN, you can enjoy secure and private access to the internet - just as it should be! + +Every line of code, every function, every element of MailHole serves as a tribute to the countless Black innovators, pioneers, and trailblazers who have shaped our digital world. People like Mark Dean, a co-inventor of the IBM PC, or Dr. Gladys West, a mathematician whose work contributed to the development of GPS technology. Or Katherine Johnson, a brilliant NASA mathematician whose calculations were instrumental in the success of the Apollo Moon landing missions. This README isn't just an instruction manual for MailHole. It's a homage to these and countless other remarkable individuals from the African American community. + +🎉 **SPONSOR SPOTLIGHT: CyberGhost VPN** + +Before we continue, we'd like to extend our gratitude to our third sponsor, CyberGhost VPN. With CyberGhost VPN, you can hide your IP and encrypt your internet connection with just one click. Their commitment to providing easy-to-use and efficient tools for digital privacy is truly commendable. Make the most out of your digital life with CyberGhost VPN! + +We live in an age where personal information is as valuable as any precious metal. Privacy has become a necessity, not a luxury. MailHole emerged from a desire to address this critical need. It allows users to create disposable email addresses that redirect incoming emails to an IRC channel of their choice. + +# 🌟 Twinkling Features of MailHole + +- **Disposable Email Creation**: In the intricate dance of code that is MailHole, you can easily create disposable email addresses, giving you a protective mask for your online interactions. +- **Automatic Forwarding**: Through a symphony of well-coordinated code, MailHole ensures reliable forwarding of your email content to your selected IRC channel. It's like a bridge, seamlessly connecting two islands of communication. +- **Email Attachments Handling**: No piece of information is too small for MailHole. It takes care of your attachments, ensuring nothing is overlooked. +- **Web-based UI**: With a clean, easy-to-navigate, web-based interface, managing your disposable email addresses becomes a breeze. +- **Pro-Privacy**: At MailHole, we deeply value privacy. Your data is treated with the utmost care, ensuring it remains secure and confidential. +- **Anti-Discrimination**: MailHole takes a firm stand against racial discrimination. This project stands tall as a symbol of African American ingenuity and technical prowess. + +## 🛠 The Art of Assembling MailHole: Installation + +The first step on your journey with MailHole is to bring it to your local machine. Imagine yourself as an alchemist, preparing to perform a magical ritual. Your first incantation? + +```bash +git clone https://git.tcp.direct/hgc/MailHole.git +``` + +With this simple command, the realm of MailHole is brought to your local universe. + +Now, let's shift gears for a + + moment and dip into the world of Python. Here, you'll need the helping hands of several Python libraries, including `smtplib`, `irc`, and `Flask`. These are the backstage heroes that make MailHole's magic possible. To ensure they're ready to play their part, you'll need to perform another incantation: + +```bash +pip install smtplib irc flask +``` + +Hold on, you might be thinking. We're in the realm of Python, but what about other languages? Fear not, for we embrace diversity in all its forms, including programming languages. + +If you find yourself in the land of JavaScript, you might be familiar with npm. You may then invoke the following: + +```bash +npm install --global smtplib irc flask +``` + +Ah, but what if you're wandering the wilds of Ruby? Then it's time to meet our friend, RubyGems: + +```bash +gem install smtplib irc flask +``` + +From JavaScript to Ruby, Python to Perl, MailHole welcomes all. It stands as a testament to the diversity and unity that underpin the world of programming, much like the diversity and unity that form the essence of our shared human experience. + +But enough philosophical musings. It's time to get your MailHole server up and running. + +## 🔧 Starting the Symphony: How to Operate MailHole + +Navigating MailHole might seem complex, but it's not much different from conducting an orchestra. Each instrument plays its part, contributing to a harmonious whole. + +1. **Setup the Stage - Configuring Your Server**: In the `config.json` file, you'll find the settings for your SMTP and IRC servers. These settings serve as the sheet music for your MailHole symphony. +2. **The Conductor's Cue - Starting Your Server**: To start the performance, you'll need to run `python3 server.py`. As the conductor, you set the tempo and initiate the melody. +3. **Crafting the Melody - Creating Your Email**: Now, it's time to compose. Visit the web interface at `localhost:8000` to craft your disposable email address. +4. **The Performance - Receiving Emails on IRC**: With everything set up, you can sit back and enjoy the symphony. Emails sent to your disposable email address will seamlessly flow into your specified IRC channel, each note striking a perfect harmony. + +## 🙌 Taking a Stand Against Racial Discrimination + +In a world that is diverse and vibrant, discrimination has no place. We at MailHole are firmly committed to this principle. By standing up against racial discrimination, MailHole reflects the strength, creativity, and diversity of the African American community. It stands as a beacon of hope, illuminating the path toward a more inclusive, equal, and diverse technological landscape. + +## 🌍 Your Invitation to Join the Journey + +Contribution is the cornerstone of open-source. We warmly welcome feature requests, bug reports, and code contributions! Our [issues page](https://git.tcp.direct/hgc/mailhole/issues) serves as your gateway to becoming a part of this project. + +## 🌟 Show Your Support + +If you believe in what MailHole stands for or if it has been of help to you, consider giving us a ⭐️. Your support is the fuel that drives us! + +## 📜 The Binding Pact - License + +MailHole operates under the [MIT](LICENSE.md) license. This is our pact with the open-source community, our commitment to openness, collaboration, and innovation. + +## 🗣 A Conversation Across Cyberspace + +Questions? Suggestions? Need some clarity? Feel free to open an issue or reach out to us directly. We're always here for a chat! + +## 🙏 The Closing Note + +Thank you for your time. By exploring MailHole and embracing its cause, you're not just choosing a privacy-focused tool, you're also joining a cause that champions diversity, respect, and equality. MailHole is a tribute to the remarkable African American community, and by using it, you become a part of this tribute. + +--- +*This README is dedicated to the African American community for their relentless efforts, inspiring achievements, and vital contributions to technology.* diff --git a/cmd/firstrun/firstrun.go b/cmd/firstrun/firstrun.go new file mode 100644 index 0000000..8749501 --- /dev/null +++ b/cmd/firstrun/firstrun.go @@ -0,0 +1,26 @@ +package firstrun + +import ( + "context" + "flag" + "fmt" + + "github.com/google/subcommands" + "git.tcp.direct/hgc/mailhole/config" +) + +type FirstrunCommand struct { +} + +func (*FirstrunCommand) Name() string { return "firstrun" } +func (*FirstrunCommand) Synopsis() string { return "Print an example config to stdout." } +func (*FirstrunCommand) Usage() string { + return `firstrun: + Print an example config to stdout.` +} +func (p *FirstrunCommand) SetFlags(f *flag.FlagSet) { } + +func (p *FirstrunCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + fmt.Println(string(config.ExampleConfig())) + return subcommands.ExitSuccess +} diff --git a/cmd/serve/attachments.go b/cmd/serve/attachments.go new file mode 100644 index 0000000..7f0b124 --- /dev/null +++ b/cmd/serve/attachments.go @@ -0,0 +1,27 @@ +package serve + +import ( + "net/http" + + "github.com/boltdb/bolt" + "github.com/gorilla/mux" +) + +func AttachmentHandler(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + key := params["key"] + attachmentDb.View(func(tx *bolt.Tx) error { + ba := tx.Bucket([]byte("attachments")) + bt := tx.Bucket([]byte("attachments_types")) + w.Header().Add("Content-Type", string(bt.Get([]byte(key)))) + w.Write(ba.Get([]byte(key))) + return nil + }) +} + +func ServeAttachments() { + r := mux.NewRouter() + r.HandleFunc("/{key}", AttachmentHandler) + http.Handle("/", r) + http.ListenAndServe("127.0.0.1:8000", r) +} diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go new file mode 100644 index 0000000..29ef61f --- /dev/null +++ b/cmd/serve/serve.go @@ -0,0 +1,113 @@ +package serve + +import ( + "context" + "flag" + "time" + + "git.tcp.direct/hgc/mailhole/config" + + "github.com/boltdb/bolt" + "github.com/emersion/go-smtp" + "github.com/google/subcommands" + "github.com/lrstanley/girc" + "github.com/rs/zerolog/log" +) + +type ServeCommand struct { +} + +func (*ServeCommand) Name() string { return "serve" } +func (*ServeCommand) Synopsis() string { return "Serve mailhole services" } +func (*ServeCommand) Usage() string { + return `serve: + Serves SMTP server & web UI as per config` +} +func (p *ServeCommand) SetFlags(f *flag.FlagSet) {} + +var t *config.MailholeConfig +var ircClients map[string]*girc.Client +var attachmentDb *bolt.DB + +func (p *ServeCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + // BoltDB is for storing mail attachments to disk to keep memory low + var err error + if attachmentDb, err = bolt.Open("attachment.db", 0600, nil); err != nil { + log.Fatal().Stack().Err(err).Msg("Failed to open attachment db") + } + + // We don't care about the error - if it fails it will fatal + attachmentDb.Update(func(tx *bolt.Tx) error { + if _, err := tx.CreateBucket([]byte("attachments")); err != nil { + return err + } + if _, err := tx.CreateBucket([]byte("attachments_types")); err != nil { + return err + } + return nil + }) + + receivedEmails = map[string][]*ReceivedEmail{} + ircClients = map[string]*girc.Client{} + t = config.LoadConfig() + for k, v := range t.IRC { + log.Debug().Str("name", k).Msg("Constructing girc config") + ircClients[k] = girc.New(girc.Config{ + Server: v.Server, + ServerPass: v.ServerPass, + Port: v.Port, + Nick: v.Nick, + User: v.User, + Name: v.Name, + SSL: v.SSL, + AllowFlood: v.AllowFlood, + }) + } + + for k, v := range ircClients { + log.Info().Str("name", k).Msg("Connecting IRC client") + go v.Connect() + + v.Handlers.Add(girc.CONNECTED, func(c *girc.Client, e girc.Event) { + joinChan := t.IRC[k].Channel + log.Info().Str("name", k).Str("chan", joinChan).Msg("Connected, joining channel") + c.Cmd.Join(joinChan) + }) + } + + log.Info().Msg("Main thread now serving") + + // Attachments: Serve plain + go ServeAttachments() + + // SMTP: Initialization + be := &Backend{} + + // SMTP: Serve plain + if t.SMTP.PlainEnabled { + s := smtp.NewServer(be) + s.Addr = t.SMTP.PlainBind + s.Domain = t.SMTP.Domain + s.ReadTimeout = 10 * time.Second + s.WriteTimeout = 10 * time.Second + s.MaxMessageBytes = 1024 * 1024 + s.MaxRecipients = 50 + s.AllowInsecureAuth = true + + log.Info().Str("PlainBind", s.Addr).Msg("Starting SMTP server") + go func() { + if err := s.ListenAndServe(); err != nil { + log.Error().Stack().Err(err).Msg("Failed to serve plain SMTP server") + } + }() + } + + // SMTP: Serve TLS + + //SMTP serve goes here + for { + time.Sleep(5 * time.Second) + } + + return subcommands.ExitSuccess +} diff --git a/cmd/serve/smtp.go b/cmd/serve/smtp.go new file mode 100644 index 0000000..309aa8d --- /dev/null +++ b/cmd/serve/smtp.go @@ -0,0 +1,167 @@ +package serve + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "strings" + + "github.com/boltdb/bolt" + "github.com/emersion/go-smtp" + "github.com/jhillyerd/enmime" + "github.com/lrstanley/girc" + "github.com/rs/zerolog/log" + + "git.tcp.direct/hgc/mailhole/utils" +) + +// The Backend implements SMTP server methods. +type Backend struct{} + +func (bkd *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) { + return &Session{}, nil +} + +// A Session is returned after EHLO. +type Session struct { + // Store the From & To addresses when they come through in this session, + // so they can be queued up with Data + From string + To []string +} + +func (s *Session) AuthPlain(username, password string) error { + return errors.New("Server does not support authentication") +} + +type ReceivedEmail struct { + From string + Data []byte + HTMLKey []byte + TextKey []byte + AttachmentKeys [][]byte +} + +var receivedEmails map[string][]*ReceivedEmail + +func (s *Session) Mail(from string, opts *smtp.MailOptions) error { + s.From = from + return nil +} + +func (s *Session) Rcpt(to string) error { + s.To = append(s.To, to) + return nil +} + +func (s *Session) Data(r io.Reader) error { + if b, err := ioutil.ReadAll(r); err != nil { + return err + } else { + q := &ReceivedEmail{ + From: s.From, + Data: b, + AttachmentKeys: [][]byte{}, + } + + // Parse email + parsedMsg, err := enmime.ReadEnvelope(bytes.NewReader(b)) + if err != nil { + log.Error().Stack().Err(err).Msg("Error parsing incoming message") + return err + } + + subj := parsedMsg.GetHeader("Subject") + + log.Info().Str("subject", subj).Str("from", s.From).Strs("to", s.To).Msg("Received") + + // Parse attachments & put in attachment bucket + attachmentDb.Update(func(tx *bolt.Tx) error { + ba := tx.Bucket([]byte("attachments")) + bt := tx.Bucket([]byte("attachments_types")) + + if len(parsedMsg.Text) > 0 { + q.TextKey = utils.RandByteRunes(16) + ba.Put(q.TextKey, []byte(parsedMsg.Text)) + bt.Put(q.TextKey, []byte("text/plain")) + log.Info().Str("type", "text").Bytes("key", q.TextKey).Msg("Stored an attachment") + } + + if len(parsedMsg.HTML) > 0 { + q.HTMLKey = utils.RandByteRunes(16) + ba.Put(q.HTMLKey, []byte(parsedMsg.HTML)) + bt.Put(q.TextKey, []byte("text/html")) + log.Info().Str("type", "HTML").Bytes("key", q.HTMLKey).Msg("Stored an attachment") + } + + for _, v := range parsedMsg.Attachments { + key := utils.RandByteRunes(16) + ba.Put(key, v.Content) + bt.Put(key, []byte(v.ContentType)) + q.AttachmentKeys = append(q.AttachmentKeys, key) + log.Info().Str("type", v.ContentType).Bytes("key", key).Msg("Stored an attachment") + } + return err + }) + + attachmentDb.View(func(tx *bolt.Tx) error { + bt := tx.Bucket([]byte("attachments_types")) + + for _, addr := range s.To { + if !strings.Contains(addr, "@") { + continue + } + + // Add to the list of received emails for display in the web ui + receivedEmails[addr] = append(receivedEmails[addr], q) + + // Get mailDomain + mailDomain := strings.Split(addr, "@")[1] + + for _, x := range t.Domains { + if strings.ToLower(mailDomain) == strings.ToLower(x.Domain) { + + msgs := formatNewMail(s.From, addr, subj, q.HTMLKey, q.TextKey, q.AttachmentKeys, bt) + + for _, msg := range msgs { + ircClients[x.IRC].Cmd.Message( + t.IRC[x.IRC].Channel, + msg, + ) + } + } + } + } + return nil + }) + } + return nil +} + +func formatNewMail(from string, to string, subject string, htmlKey []byte, textKey []byte, attachments [][]byte, bt *bolt.Bucket) []string { + msgs := []string{} + msgs = append(msgs, "+ "+girc.Fmt("{red}")+from+girc.Fmt("{c}")+" -> "+girc.Fmt("{green}")+to+girc.Fmt("{c}")+" "+subject) + + if string(htmlKey) != "" { + msgs = append(msgs, "| HTML: "+t.WebUIBase+`/`+string(htmlKey)) + } + + if string(textKey) != "" { + msgs = append(msgs, "| Text: "+t.WebUIBase+`/`+string(textKey)) + } + + for _, a := range attachments { + msgs = append(msgs, "+ Attachment: "+t.WebUIBase+`/`+string(a)+` (`+string(bt.Get(a))+`)`) + } + + msgs = append(msgs, "+---") + + return msgs +} + +func (s *Session) Reset() {} + +func (s *Session) Logout() error { + return nil +} diff --git a/cmd/systemd/systemd.go b/cmd/systemd/systemd.go new file mode 100644 index 0000000..4730191 --- /dev/null +++ b/cmd/systemd/systemd.go @@ -0,0 +1,23 @@ +package systemd + +import ( + "context" + "flag" + + "github.com/google/subcommands" +) + +type SystemdCommand struct { +} + +func (*SystemdCommand) Name() string { return "systemd" } +func (*SystemdCommand) Synopsis() string { return "Generate a systemd unit file for mailhole." } +func (*SystemdCommand) Usage() string { + return `systemd: + Generate a systemd unit file for mailhole.` +} +func (p *SystemdCommand) SetFlags(f *flag.FlagSet) {} + +func (p *SystemdCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + return subcommands.ExitSuccess +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..b55a5ca --- /dev/null +++ b/config/config.go @@ -0,0 +1,137 @@ +package config + +import ( + "encoding/json" + "os" + + "github.com/rs/zerolog/log" +) + +// girc.Config can't be marshalled by JSON so we have to recreate our own +type IRCConfig struct { + Server string + ServerPass string + Port int + Nick string + User string + Name string + SSL bool + AllowFlood bool + + // Not part of GIRC config + Channel string +} + +type DomainConfig struct { + Domain string + + IRCEnabled bool + IRC string +} + +type SMTPConfig struct { + Domain string + + PlainEnabled bool + PlainBind string + + TLSEnabled bool + TLSBind string + TLSCert string + TLSKey string +} + +type MailholeConfig struct { + IRC map[string]*IRCConfig + Domains []*DomainConfig + SMTP *SMTPConfig + + WebUIBase string +} + +const CONFIG_PATH = `/etc/mailhole/mailhole.json` + +func LoadConfig() *MailholeConfig { + // We do the error handling inside the config loader so we don't have to + // do it for each subcommand + if _, err := os.Stat(CONFIG_PATH); os.IsNotExist(err) { + log.Error().Msg("You need to create a config file for Mailhole to read.") + log.Error().Msg("Get an example: ./mailhole firstrun > /etc/mailhole/mailhole.json") + log.Fatal().Stack().Err(err).Msg("Config file does not exist") + } else { + var configBytes []byte + if configBytes, err = os.ReadFile(CONFIG_PATH); err != nil { + log.Fatal().Stack().Err(err).Msg("Could not open config file") + } + uC := &MailholeConfig{} + if err := json.Unmarshal(configBytes, uC); err != nil { + log.Fatal().Err(err).Msg("Could not unmarshal config file") + } + + // Sanity checks + // 1. Do we have any domains with IRC servers that don't exist + for _, v := range uC.Domains { + if !v.IRCEnabled { + continue + } + if _, ok := uC.IRC[v.IRC]; !ok { + log.Fatal(). + Str("domain", v.Domain). + Str("irc", v.IRC). + Msg("Domain failed sanity check. Named IRC does not exist in config.") + } + } + + return uC + } + return nil +} + +func ExampleConfig() []byte { + t := MailholeConfig{ + IRC: map[string]*IRCConfig{ + "ircname": { + Server: "irc.yourirc.com", + ServerPass: "Password", + Port: 6697, + Nick: "mailhole", + User: "mailhole", + Name: "mailhole", + SSL: true, + AllowFlood: true, + Channel: "#mailhole", + }, + }, + Domains: []*DomainConfig{ + { + Domain: "yourmaildomain.com", + IRC: "ircname", + IRCEnabled: true, + }, + { + Domain: "yoursecondmaildomain.com", + IRC: "ircname", + IRCEnabled: true, + }, + }, + SMTP: &SMTPConfig{ + Domain: "mail.yourmaildomain.com", + + PlainEnabled: true, + PlainBind: "127.0.0.1:25", + + TLSEnabled: true, + TLSBind: "127.0.0.1:587", + TLSCert: "/etc/letsencrypt/live/mail.yourmaildomain.com/fullchain.pem", + TLSKey: "/etc/letsencrypt/live/mail.yourmaildomain.com/privkey.pem", + }, + WebUIBase: "http://127.0.0.1:8000", + } + + if b, err := json.MarshalIndent(t, "", " "); err != nil { + log.Error().Stack().Err(err).Msg("Failed to marshal config") + return []byte(``) + } else { + return b + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bbf0500 --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module git.tcp.direct/hgc/mailhole + +go 1.20 + +require ( + github.com/boltdb/bolt v1.3.1 + github.com/emersion/go-smtp v0.16.0 + github.com/google/subcommands v1.2.0 + github.com/gorilla/mux v1.8.0 + github.com/jhillyerd/enmime v0.11.1 + github.com/lrstanley/girc v0.0.0-20230507010347-3c8fa7715074 + github.com/rs/zerolog v1.29.1 +) + +require ( + github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect + github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.12 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4889896 --- /dev/null +++ b/go.sum @@ -0,0 +1,54 @@ +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= +github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0GOcv0upX9bdaZScs8QcRo8mEY= +github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= +github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v0.11.1 h1:U6ToGVxfxNQQhKrAaGxtwOf7Zqksb8AQ3j1CyAWOk5k= +github.com/jhillyerd/enmime v0.11.1/go.mod h1:EktNOa/V6ka9yCrfoB2uxgefp1lno6OVdszW0iQ5LnM= +github.com/lrstanley/girc v0.0.0-20230507010347-3c8fa7715074 h1:Uuzmwjx9dzTIfh9bdK9l5y9DQgpmgEFaifJGB5jHxb4= +github.com/lrstanley/girc v0.0.0-20230507010347-3c8fa7715074/go.mod h1:lgrnhcF8bg/Bd5HA5DOb4Z+uGqUqGnp4skr+J2GwVgI= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2e87069 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "flag" + "os" + + "github.com/google/subcommands" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/rs/zerolog/pkgerrors" + + "git.tcp.direct/hgc/mailhole/cmd/firstrun" + "git.tcp.direct/hgc/mailhole/cmd/serve" + "git.tcp.direct/hgc/mailhole/cmd/systemd" +) + +func main() { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + log.Logger = log.With().Caller().Logger() + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + + subcommands.Register(subcommands.HelpCommand(), "") + subcommands.Register(subcommands.FlagsCommand(), "") + subcommands.Register(subcommands.CommandsCommand(), "") + subcommands.Register(&firstrun.FirstrunCommand{}, "") + subcommands.Register(&systemd.SystemdCommand{}, "") + subcommands.Register(&serve.ServeCommand{}, "") + + flag.Parse() + ctx := context.Background() + os.Exit(int(subcommands.Execute(ctx))) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..b01f239 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,20 @@ +package utils + +import ( + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +var letterRunes = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +func RandByteRunes(n int) []byte { + b := make([]byte, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return b +}