├── .env.example ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── src ├── cmd ├── add │ └── add.go ├── install │ └── install.go ├── ping │ └── ping.go ├── remove │ └── remove.go ├── root.go ├── show │ └── show.go └── update │ └── update.go ├── data ├── servers.go └── users.go ├── infrastructure ├── config │ └── configuration.go └── database │ └── database.go ├── providers └── mail │ ├── mail.go │ └── templates │ └── templates.go └── use_cases ├── ping └── ping.go └── sendMail └── sendMail.go /.env.example: -------------------------------------------------------------------------------- 1 | SMPT_SERVER = "smtp.gmail.com" 2 | SMTP_PORT = 465 3 | SMTP_USER = "yourmail@gmail.com" 4 | SMTP_PASSWORD = "password" 5 | SMTP_INSECURE = True 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/moohbr/WebMonitor.svg)](https://pkg.go.dev/github.com/moohbr/WebMonitor) 2 | # WebMonitor 3 | 4 | Faced with a considerable number of tools that monitor your site, why use WebMonitor? Why use a lightweight, easy and cost-free tool? I don't know either. 5 | 6 | WebMonitor emerged from a need to monitor my infrastructure in a simple and effective way. 7 | It's a tool made by a university student 'n intern, so it's constantly improving. 8 | 9 | ## Features 10 | 11 | - [x] Monitor your site 12 | - [x] Monitor your site with a custom user-agent 13 | - [x] Notify you when your site is down 14 | 15 | ## Future features 16 | 17 | - [ ] Monitor your site with a custom user-agent 18 | - [ ] Notify with custom message/custom interval/custom timeout 19 | - [ ] Create a web interface to manage the application 20 | - [ ] User profiles and authentication 21 | 22 | 23 | ## Contributing 24 | 25 | If you want to contribute to this project, you can do it in two ways: 26 | 27 | 1. Open an issue with a bug report or a feature request 28 | 2. Open a pull request with a bug fix or a new feature 29 | 30 | ## License 31 | 32 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 33 | 34 | 35 | ## Documentation 36 | ### Environment Variables 37 | 38 | To run this project, you will need to add the following environment variables to your `.env` file: 39 | 40 | - `SMPT_SERVER` - SMTP server address 41 | - `SMTP_PORT` - SMTP server port 42 | - `SMTP_USER` - SMTP server user 43 | - `SMTP_PASSWORD` - SMTP server password 44 | - `SMTP_INSECURE` - SMTP server insecure 45 | 46 | ### CLI Arguments 47 | 48 | ``` 49 | Available Commands: 50 | install Install database for first time 51 | show Show a list of servers or users 52 | add Add a new site to monitor or user to notify 53 | remove Remove a site or user 54 | update Update a site or user 55 | help Help about any command 56 | 57 | Use "WebMonitor [command] --help" for more information about a command. 58 | 59 | Flags: 60 | -h, --help help for WebMonitor 61 | -v, --verbose verbose output 62 | ``` 63 | 64 | ### Installation 65 | 66 | 67 | #### Manual 68 | 69 | 1. Clone the repository 70 | 2. Install the dependencies with `go build` 71 | 3. Run the script with `WebMonitor` 72 | 73 | ### Usage 74 | 75 | 76 | #### Manual 77 | 78 | 1. Create a `.env` file. 79 | 2. Install the dependencies with `go build` and run the script with `webmonitor` 80 | 81 | ## Questions 82 | 83 | If you have any questions, feel free to open an issue or contact me on [email](mailto:moohbr@gmail.com). 84 | 85 | ## Acknowledgements 86 | 87 | - [Cobra](github.com/spf13/cobra) 88 | - [SQLite](github.com/mattn/go-sqlite3) 89 | - [go-mail](github.com/go-mail/mail) 90 | - [go-dotenv](github.com/joho/godotenv) 91 | 92 | 93 | ## Authors 94 | 95 | - [@moohbr](https://www.github.com/moohbr) 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moohbr/WebMonitor 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/joho/godotenv v1.4.0 7 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 8 | github.com/olekukonko/tablewriter v0.0.5 9 | github.com/spf13/cobra v1.5.0 10 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 11 | gopkg.in/mail.v2 v2.3.1 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 3 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 4 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 5 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 6 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 7 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 8 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 9 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 10 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 11 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 12 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 13 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 14 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 15 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 16 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 17 | github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= 18 | github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 19 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 20 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 21 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 22 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 23 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 24 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 25 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= 28 | gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= 29 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | cmd "github.com/moohbr/WebMonitor/src/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.RootCmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /src/cmd/add/add.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "sync" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | data "github.com/moohbr/WebMonitor/src/data" 12 | database "github.com/moohbr/WebMonitor/src/infrastructure/database" 13 | templates "github.com/moohbr/WebMonitor/src/providers/mail/templates" 14 | mail "github.com/moohbr/WebMonitor/src/use_cases/sendMail" 15 | ) 16 | 17 | var ( 18 | wg sync.WaitGroup 19 | verbose bool 20 | 21 | AddCmd = &cobra.Command{ 22 | Use: "add", 23 | Short: "Add something", 24 | Long: `With this command you can add something, like the servers or the users.`, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | log.Println("[HELP] Add something use the subcommands") 27 | log.Println("[HELP] Use 'add server' to add a server") 28 | log.Println("[HELP] Use 'add user' to add a user") 29 | }, 30 | } 31 | 32 | addServerCmd = &cobra.Command{ 33 | Use: "server", 34 | Short: "Add a server", 35 | Long: `With this command you can add a server.`, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | defer wg.Wait() 38 | 39 | log.Println("[SYSTEM] Adding a server") 40 | name := cmd.Flag("name").Value.String() 41 | ip := cmd.Flag("ip").Value.String() 42 | url := cmd.Flag("url").Value.String() 43 | 44 | server := data.Server{ 45 | Name: name, 46 | IP: ip, 47 | URL: url, 48 | AvarageResponseTime: 0, 49 | LastUpdate: time.Now().UTC().Format("2006-01-02 15:04:05"), 50 | LastCheck: time.Now().UTC().Format("2006-01-02 15:04:05"), 51 | LastStatus: 0, 52 | Monitor: true, 53 | } 54 | 55 | db := database.OpenDatabase() 56 | defer db.Close() 57 | db.AddServer(server) 58 | 59 | users := db.GetUsers() 60 | wg.Add(len(users)) 61 | if len(users) > 0 { 62 | for _, user := range users { 63 | go mail.SendMail([]string{user.Email}, templates.NewServer(server), &wg) 64 | } 65 | } else { 66 | log.Println("[WARNING] No users to send the email") 67 | } 68 | 69 | log.Println("[SYSTEM] Server added") 70 | }, 71 | } 72 | 73 | addUserCmd = &cobra.Command{ 74 | Use: "user", 75 | Short: "Add a user", 76 | Long: `With this command you can add a user.`, 77 | Run: func(cmd *cobra.Command, args []string) { 78 | log.Println("[SYSTEM] Adding a user") 79 | name := cmd.Flag("name").Value.String() 80 | password := cmd.Flag("password").Value.String() 81 | email := cmd.Flag("email").Value.String() 82 | 83 | admin := cmd.Flag("admin").Value.String() 84 | 85 | adminBool, err := strconv.ParseBool(admin) 86 | if err != nil { 87 | log.Println("[ERROR] Error parsing the admin flag") 88 | log.Println("[ERROR] Error: ", err) 89 | return 90 | } 91 | 92 | user := data.User{ 93 | Name: name, 94 | Password: password, 95 | Email: email, 96 | Admin: adminBool, 97 | LastLogin: time.Now(), 98 | LastNotif: time.Now(), 99 | } 100 | 101 | db := database.OpenDatabase() 102 | db.AddUser(user) 103 | defer db.Close() 104 | 105 | log.Println("[SYSTEM] User added") 106 | }, 107 | } 108 | ) 109 | 110 | func init() { 111 | AddCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") 112 | 113 | AddCmd.AddCommand(addServerCmd) 114 | addServerCmd.PersistentFlags().String("name", "", "The name of the server") 115 | addServerCmd.PersistentFlags().String("ip", "", "The ip of the server") 116 | addServerCmd.PersistentFlags().String("url", "", "The url of the server") 117 | addServerCmd.MarkPersistentFlagRequired("name") 118 | addServerCmd.MarkPersistentFlagRequired("ip") 119 | addServerCmd.MarkPersistentFlagRequired("url") 120 | addServerCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") 121 | 122 | AddCmd.AddCommand(addUserCmd) 123 | addUserCmd.PersistentFlags().String("name", "", "The name of the user") 124 | addUserCmd.PersistentFlags().String("password", "", "The password of the user") 125 | addUserCmd.PersistentFlags().String("email", "", "The email of the user") 126 | addUserCmd.PersistentFlags().Bool("admin", false, "The admin of the user") 127 | addUserCmd.MarkPersistentFlagRequired("name") 128 | addUserCmd.MarkPersistentFlagRequired("password") 129 | addUserCmd.MarkPersistentFlagRequired("email") 130 | addUserCmd.MarkPersistentFlagRequired("admin") 131 | addUserCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") 132 | } 133 | -------------------------------------------------------------------------------- /src/cmd/install/install.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | database "github.com/moohbr/WebMonitor/src/infrastructure/database" 9 | ) 10 | 11 | var ( 12 | verbose bool 13 | InstallCmd = &cobra.Command{ 14 | Use: "install", 15 | Short: "Install database", 16 | Long: `With this command you can install to storage data, like the servers or the users.`, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | log.Println("[SYSTEM] Installing database") 19 | database.NewDatabase() 20 | log.Println("[SYSTEM] Database installed") 21 | }, 22 | } 23 | ) 24 | 25 | func init() { 26 | InstallCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") 27 | } 28 | -------------------------------------------------------------------------------- /src/cmd/ping/ping.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | ping "github.com/moohbr/WebMonitor/src/use_cases/ping" 7 | ) 8 | 9 | var ( 10 | verbose bool 11 | 12 | PingCmD = &cobra.Command{ 13 | Use: "ping", 14 | Short: "Ping a server", 15 | Long: `With this command you can ping a server.`, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | ping.PingAllServers() 18 | }, 19 | } 20 | ) 21 | 22 | func init() { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/cmd/remove/remove.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | database "github.com/moohbr/WebMonitor/src/infrastructure/database" 9 | ) 10 | 11 | var ( 12 | verbose bool 13 | 14 | RemoveCmd = &cobra.Command{ 15 | Use: "remove", 16 | Short: "Remove something", 17 | Long: `With this command you can remove something, like the servers or the users.`, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | log.Println("[HELP] Remove something use the subcommands") 20 | log.Println("[HELP] Use 'remove server' to remove a server") 21 | log.Println("[HELP] Use 'remove user' to remove a user") 22 | }, 23 | } 24 | 25 | removeServerCmd = &cobra.Command{ 26 | Use: "server", 27 | Short: "Remove a server", 28 | Long: `With this command you can remove a server.`, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | log.Println("[SYSTEM] Removing a server") 31 | 32 | name, _ := cmd.Flags().GetString("name") 33 | db := database.OpenDatabase() 34 | db.DeleteServer(name) 35 | }, 36 | } 37 | 38 | removeUserCmd = &cobra.Command{ 39 | Use: "user", 40 | Short: "Remove a user", 41 | Long: `With this command you can remove a user.`, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | log.Println("[SYSTEM] Removing a user") 44 | 45 | name, _ := cmd.Flags().GetString("name") 46 | db := database.OpenDatabase() 47 | db.DeleteUser(name) 48 | }, 49 | } 50 | ) 51 | 52 | func init() { 53 | RemoveCmd.AddCommand(removeServerCmd) 54 | 55 | removeServerCmd.Flags().StringP("name", "n", "", "The name of the server") 56 | removeServerCmd.MarkPersistentFlagRequired("name") 57 | removeServerCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 58 | 59 | RemoveCmd.AddCommand(removeUserCmd) 60 | removeUserCmd.Flags().StringP("name", "n", "", "The name of the user") 61 | removeServerCmd.MarkPersistentFlagRequired("name") 62 | removeUserCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 63 | } 64 | -------------------------------------------------------------------------------- /src/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | add "github.com/moohbr/WebMonitor/src/cmd/add" 9 | install "github.com/moohbr/WebMonitor/src/cmd/install" 10 | ping "github.com/moohbr/WebMonitor/src/cmd/ping" 11 | remove "github.com/moohbr/WebMonitor/src/cmd/remove" 12 | show "github.com/moohbr/WebMonitor/src/cmd/show" 13 | update "github.com/moohbr/WebMonitor/src/cmd/update" 14 | ) 15 | 16 | var ( 17 | verbose bool 18 | 19 | RootCmd = &cobra.Command{ 20 | Use: "WebMonitor", 21 | Short: "WebMonitor is a tool to monitor websites", 22 | Long: `WebMonitor is a tool to monitor websites. 23 | It will ping the websites and send a report by email.`, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | log.Println("WebMonitor was developed by Matheus Araujo a.k.a. moohbr") 26 | }, 27 | } 28 | ) 29 | 30 | func init() { 31 | 32 | RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") 33 | 34 | RootCmd.AddCommand(show.ShowCmd) 35 | RootCmd.AddCommand(add.AddCmd) 36 | RootCmd.AddCommand(update.UpdateCmd) 37 | RootCmd.AddCommand(remove.RemoveCmd) 38 | RootCmd.AddCommand(install.InstallCmd) 39 | RootCmd.AddCommand(ping.PingCmD) 40 | } 41 | -------------------------------------------------------------------------------- /src/cmd/show/show.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | 8 | database "github.com/moohbr/WebMonitor/src/infrastructure/database" 9 | "github.com/olekukonko/tablewriter" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | verbose bool 15 | ShowCmd = &cobra.Command{ 16 | Use: "show", 17 | Short: "Show something", 18 | Long: `With this command you can show something, like the servers or the users.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | log.Println("[HELP] Add something use the subcommands") 21 | log.Println("[HELP] Use 'show server' to show a server") 22 | log.Println("[HELP] Use 'show user' to show a user") 23 | }, 24 | } 25 | 26 | showServerCmd = &cobra.Command{ 27 | Use: "server", 28 | Short: "Show a server", 29 | Long: `With this command you can show a server.`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | if verbose { 32 | log.Println("[SYSTEM] Showing a server") 33 | } 34 | db := database.NewDatabase() 35 | server := db.GetServer(cmd.Flag("name").Value.String()) 36 | if server.Name != "" { 37 | table := tablewriter.NewWriter(os.Stdout) 38 | table.SetHeader([]string{"Name", "IP", "URL", "Avarage Response Time", "Last Update", "Last Check", "Last Status", "Monitor"}) 39 | laststatus := strconv.Itoa(server.LastStatus) 40 | table.Append([]string{server.Name, server.IP, server.URL, server.AvarageResponseTime.String(), 41 | server.LastUpdate, server.LastCheck, laststatus, strconv.FormatBool(server.Monitor)}) 42 | defer table.Render() 43 | } else { 44 | log.Println("[SYSTEM] No server found!") 45 | } 46 | 47 | }, 48 | } 49 | 50 | showServersCmd = &cobra.Command{ 51 | Use: "servers", 52 | Short: "Show all servers", 53 | Long: `With this command you can show all servers.`, 54 | Run: func(cmd *cobra.Command, args []string) { 55 | db := database.NewDatabase() 56 | 57 | servers := db.GetServers() 58 | if len(servers) > 0 { 59 | table := tablewriter.NewWriter(os.Stdout) 60 | table.SetHeader([]string{"Name", "IP", "URL", "Avarage Response Time", "Last Update", "Last Check", "Last Status", "Monitor"}) 61 | for _, server := range servers { 62 | laststatus := strconv.Itoa(server.LastStatus) 63 | table.Append([]string{server.Name, server.IP, server.URL, server.AvarageResponseTime.String(), 64 | server.LastUpdate, server.LastCheck, laststatus, strconv.FormatBool(server.Monitor)}) 65 | } 66 | defer table.Render() 67 | } else { 68 | log.Println("[SYSTEM] No servers found!") 69 | } 70 | }, 71 | } 72 | 73 | showUsersCmd = &cobra.Command{ 74 | Use: "users", 75 | Short: "Show all users", 76 | Long: `With this command you can show all users.`, 77 | Run: func(cmd *cobra.Command, arg []string) { 78 | db := database.NewDatabase() 79 | users := db.GetUsers() 80 | if len(users) > 0 { 81 | table := tablewriter.NewWriter(os.Stdout) 82 | table.SetHeader([]string{"Name", "Email", "Password", "Admin"}) 83 | for _, user := range users { 84 | table.Append([]string{user.Name, user.Email, user.Password, strconv.FormatBool(user.Admin)}) 85 | } 86 | defer table.Render() 87 | } else { 88 | log.Println("[SYSTEM] No users found!") 89 | } 90 | }, 91 | } 92 | ) 93 | 94 | func init() { 95 | ShowCmd.AddCommand(showServersCmd) 96 | showServerCmd.Flags().StringP("name", "n", "", "The name of the server") 97 | showServerCmd.MarkFlagRequired("name") 98 | showServerCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show more information") 99 | 100 | ShowCmd.AddCommand(showServerCmd) 101 | ShowCmd.AddCommand(showUsersCmd) 102 | } 103 | -------------------------------------------------------------------------------- /src/cmd/update/update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | data "github.com/moohbr/WebMonitor/src/data" 10 | database "github.com/moohbr/WebMonitor/src/infrastructure/database" 11 | ) 12 | 13 | var ( 14 | verbose bool 15 | 16 | UpdateCmd = &cobra.Command{ 17 | Use: "update", 18 | Short: "Update something", 19 | Long: `With this command you can update something, like the servers or the users.`, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | log.Println("[HELP] Update something use the subcommands") 22 | log.Println("[HELP] Use 'update server' to update a server") 23 | log.Println("[HELP] Use 'update user' to update a user") 24 | }, 25 | } 26 | 27 | updateServerCmd = &cobra.Command{ 28 | Use: "server", 29 | Short: "Update a server", 30 | Long: `With this command you can update a server.`, 31 | Run: func(cmd *cobra.Command, args []string) { 32 | log.Println("[SYSTEM] Updating a server") 33 | 34 | name, _ := cmd.Flags().GetString("name") 35 | ip, _ := cmd.Flags().GetString("ip") 36 | url, _ := cmd.Flags().GetString("url") 37 | monitor, _ := cmd.Flags().GetBool("monitor") 38 | 39 | server := data.Server{ 40 | Name: name, 41 | IP: ip, 42 | URL: url, 43 | Monitor: monitor, 44 | LastUpdate: time.Now().UTC().Format("2006-01-02 15:04:05"), 45 | } 46 | 47 | db := database.OpenDatabase() 48 | db.UpdateServer(server) 49 | }, 50 | } 51 | 52 | updateUserCmd = &cobra.Command{ 53 | Use: "user", 54 | Short: "Update a user", 55 | Long: `With this command you can update a user.`, 56 | Run: func(cmd *cobra.Command, args []string) { 57 | log.Println("[SYSTEM] Updating a user") 58 | 59 | name, _ := cmd.Flags().GetString("name") 60 | password, _ := cmd.Flags().GetString("password") 61 | 62 | user := data.User{ 63 | Name: name, 64 | Password: password, 65 | } 66 | 67 | db := database.OpenDatabase() 68 | db.UpdateUser(user) 69 | }, 70 | } 71 | ) 72 | 73 | func init() { 74 | UpdateCmd.AddCommand(updateServerCmd) 75 | 76 | updateServerCmd.Flags().StringP("name", "n", "", "The name of the server") 77 | updateServerCmd.Flags().StringP("ip", "i", "", "The ip of the server") 78 | updateServerCmd.Flags().StringP("url", "u", "", "The url of the server") 79 | updateServerCmd.Flags().BoolP("monitor", "m", false, "If the server should be monitored") 80 | updateServerCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 81 | 82 | UpdateCmd.AddCommand(updateUserCmd) 83 | updateUserCmd.Flags().StringP("name", "n", "", "The name of the user") 84 | updateUserCmd.Flags().StringP("password", "p", "", "The password of the user") 85 | updateUserCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 86 | } 87 | -------------------------------------------------------------------------------- /src/data/servers.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "time" 4 | 5 | type Server struct { 6 | Name string 7 | IP string 8 | URL string 9 | AvarageResponseTime time.Duration 10 | LastUpdate string 11 | LastCheck string 12 | LastStatus int 13 | Monitor bool 14 | } 15 | -------------------------------------------------------------------------------- /src/data/users.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "time" 4 | 5 | type User struct { 6 | Name string 7 | Password string 8 | Email string 9 | Admin bool 10 | LastLogin time.Time 11 | LastNotif time.Time 12 | } 13 | -------------------------------------------------------------------------------- /src/infrastructure/config/configuration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | 8 | dotenv "github.com/joho/godotenv" 9 | ) 10 | 11 | func LoadEnv() { 12 | err := dotenv.Load() 13 | if err != nil { 14 | log.Fatal("Error loading .env file") 15 | } 16 | } 17 | 18 | func GetEnv(key string) string { 19 | return os.Getenv(key) 20 | } 21 | 22 | func ConvertToInt(value string) int { 23 | valueInt, err := strconv.Atoi(value) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | return valueInt 28 | } 29 | 30 | func ConvertToBool(value string) bool { 31 | valueBool, err := strconv.ParseBool(value) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | return valueBool 36 | } 37 | -------------------------------------------------------------------------------- /src/infrastructure/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/mattn/go-sqlite3" 8 | data "github.com/moohbr/WebMonitor/src/data" 9 | ) 10 | 11 | // Database is the database struct 12 | type Database struct { 13 | *sql.DB 14 | } 15 | 16 | // NewDatabase creates a new database 17 | func NewDatabase() *Database { 18 | sqlite3.Version() 19 | db, err := sql.Open("sqlite3", "file:database.db?cache=shared&mode=rwc&parseTime=true") 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS servers (name TEXT PRIMARY KEY, ip TEXT, url TEXT, avarageResponseTime TEXT, " + 24 | "lastUpdate TEXT , lastCheck TEXT , lastStatus TEXT, monitor BOOLEAN)") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY, password TEXT, email TEXT," + 29 | "admin BOOLEAN, lastLogin TEXT, lastNotif TEXT)") 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | return &Database{db} 34 | } 35 | 36 | func OpenDatabase() *Database { 37 | db, err := sql.Open("sqlite3", "file:database.db?cache=shared&mode=rwc&parseTime=true") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | return &Database{db} 42 | } 43 | 44 | // AddServer adds a server to the database 45 | func (db *Database) AddServer(s data.Server) { 46 | _, err := db.Exec("INSERT INTO servers VALUES (?, ?, ?, ?, ?, ?, ?, ?)", 47 | s.Name, s.IP, s.URL, s.AvarageResponseTime, s.LastUpdate, s.LastCheck, s.LastStatus, s.Monitor) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | } 52 | 53 | // AddUser adds a user to the database 54 | func (db *Database) AddUser(u data.User) { 55 | _, err := db.Exec("INSERT INTO users VALUES (?, ?, ?, ?, ?, ?)", u.Name, u.Password, u.Email, u.Admin, u.LastLogin.Unix(), u.LastNotif.Unix()) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | } 60 | 61 | // GetServer gets a server from the database 62 | func (db *Database) GetServer(name string) data.Server { 63 | var s data.Server 64 | 65 | err := db.QueryRow("SELECT * FROM servers WHERE name=?", name).Scan(&s.Name, &s.IP, &s.URL, &s.AvarageResponseTime, 66 | &s.LastUpdate, &s.LastCheck, &s.LastStatus, &s.Monitor) 67 | 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | return s 72 | } 73 | 74 | // GetUser gets a user from the database 75 | func (db *Database) GetUser(name string) data.User { 76 | var u data.User 77 | 78 | err := db.QueryRow("SELECT * FROM users WHERE name=?", name).Scan(&u.Name, &u.Password, &u.Email, &u.Admin, &u.LastLogin, &u.LastNotif) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | return u 83 | } 84 | 85 | // GetServers gets all servers from the database 86 | func (db *Database) GetServers() []data.Server { 87 | rows, err := db.Query("SELECT * FROM servers") 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | var servers []data.Server 92 | 93 | for rows.Next() { 94 | var s data.Server 95 | 96 | err = rows.Scan(&s.Name, &s.IP, &s.URL, &s.AvarageResponseTime, &s.LastUpdate, &s.LastCheck, &s.LastStatus, &s.Monitor) 97 | 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | servers = append(servers, s) 102 | } 103 | return servers 104 | } 105 | 106 | // GetUsers gets all users from the database 107 | func (db *Database) GetUsers() []data.User { 108 | rows, err := db.Query("SELECT * FROM users") 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | var users []data.User 113 | 114 | for rows.Next() { 115 | var u data.User 116 | lastlogin := u.LastLogin.String() 117 | lastnotif := u.LastNotif.String() 118 | err = rows.Scan(&u.Name, &u.Password, &u.Email, &u.Admin, &lastlogin, &lastnotif) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | users = append(users, u) 123 | } 124 | return users 125 | } 126 | 127 | // UpdateServer updates a server in the database 128 | func (db *Database) UpdateServer(s data.Server) { 129 | _, err := db.Exec("UPDATE servers SET ip=?, url=?, avarageResponseTime=?, lastUpdate=?, lastCheck=?, lastStatus=?, monitor=? WHERE name=?", 130 | s.IP, s.URL, s.AvarageResponseTime, s.LastUpdate, s.LastCheck, s.LastStatus, s.Monitor, s.Name) 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | } 135 | 136 | // UpdateUser updates a user in the database 137 | func (db *Database) UpdateUser(u data.User) { 138 | _, err := db.Exec("UPDATE users SET password=?, email=?, admin=?, lastLogin=?, lastNotif=? WHERE name=?", 139 | u.Password, u.Email, u.Admin, u.LastLogin, u.LastNotif, u.Name) 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | } 144 | 145 | // DeleteServer deletes a server from the database 146 | func (db *Database) DeleteServer(name string) { 147 | _, err := db.Exec("DELETE FROM servers WHERE name=?", name) 148 | if err != nil { 149 | log.Fatal(err) 150 | } 151 | } 152 | 153 | // DeleteUser deletes a user from the database 154 | func (db *Database) DeleteUser(name string) { 155 | _, err := db.Exec("DELETE FROM users WHERE name=?", name) 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | } 160 | 161 | // Close closes the database 162 | func (db *Database) Close() { 163 | db.DB.Close() 164 | } 165 | -------------------------------------------------------------------------------- /src/providers/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | config "github.com/moohbr/WebMonitor/src/infrastructure/config" 7 | gomail "gopkg.in/mail.v2" 8 | ) 9 | 10 | type Mail struct { 11 | From string 12 | To []string 13 | Subject string 14 | Body string 15 | } 16 | 17 | func NewMail(to []string, subject string, body string) *Mail { 18 | return &Mail{To: to, Subject: subject, Body: body} 19 | } 20 | 21 | func (mail *Mail) Send() error { 22 | config.LoadEnv() 23 | message := gomail.NewMessage() 24 | 25 | message.SetHeader("From", config.GetEnv("SMTP_USER")) 26 | message.SetHeader("To", mail.To...) 27 | 28 | message.SetHeader("Subject", mail.Subject) 29 | 30 | message.SetBody("text/plain", mail.Body) 31 | 32 | dialer := gomail.NewDialer(config.GetEnv("SMPT_SERVER"), config.ConvertToInt(config.GetEnv("SMTP_PORT")), 33 | config.GetEnv("SMTP_USER"), config.GetEnv("SMTP_PASSWORD")) 34 | 35 | dialer.TLSConfig = &tls.Config{InsecureSkipVerify: config.ConvertToBool(config.GetEnv("SMTP_INSECURE"))} 36 | 37 | if err := dialer.DialAndSend(message); err != nil { 38 | panic(err) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /src/providers/mail/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | data "github.com/moohbr/WebMonitor/src/data" 5 | ) 6 | 7 | type Mail struct { 8 | Subject string 9 | Body string 10 | } 11 | 12 | // Templates for the mails 13 | 14 | var ServerDown = func(server data.Server) Mail { 15 | return Mail{ 16 | Subject: "WebMonitor - Server Down", 17 | Body: "The server " + server.Name + " is down", 18 | } 19 | } 20 | 21 | var ServerUp = func(server data.Server) Mail { 22 | return Mail{ 23 | Subject: "WebMonitor - Server Up", 24 | Body: "The server " + server.Name + " is up", 25 | } 26 | } 27 | 28 | var NewServer = func(server data.Server) Mail { 29 | return Mail{ 30 | Subject: "WebMonitor - New Server", 31 | Body: "The server " + server.Name + " was added", 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/use_cases/ping/ping.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | "sync" 8 | "time" 9 | 10 | data "github.com/moohbr/WebMonitor/src/data" 11 | database "github.com/moohbr/WebMonitor/src/infrastructure/database" 12 | templates "github.com/moohbr/WebMonitor/src/providers/mail/templates" 13 | mail "github.com/moohbr/WebMonitor/src/use_cases/sendMail" 14 | ) 15 | 16 | var ( 17 | wg sync.WaitGroup 18 | ) 19 | 20 | func PingServer(server data.Server) { 21 | defer wg.Wait() 22 | 23 | if server.Monitor { 24 | start := time.Now() 25 | response, err := http.Get("https://" + server.URL) 26 | if err != nil { 27 | log.Println("[ERROR] Error pinging server: " + server.Name) 28 | log.Println("[ERROR] Error: " + err.Error()) 29 | server.LastStatus = 0 30 | } else { 31 | server.LastStatus = response.StatusCode 32 | } 33 | 34 | server.LastCheck = time.Now().UTC().Format("2006-01-02 15:04:05") 35 | server.AvarageResponseTime = time.Now().Sub(start) 36 | 37 | log.Println("[SYSTEM] " + server.Name + " | " + server.IP + " | " + server.URL + " | " + 38 | strconv.FormatInt(server.AvarageResponseTime.Milliseconds(), 10) + "ms | " + server.LastUpdate + " | " + server.LastCheck + 39 | " | " + strconv.Itoa(server.LastStatus) + " | " + strconv.FormatBool(server.Monitor)) 40 | db := database.OpenDatabase() 41 | defer db.Close() 42 | 43 | db.UpdateServer(server) 44 | users := db.GetUsers() 45 | 46 | if len(users) > 0 { 47 | if server.LastStatus != 200 { 48 | wg.Add(len(users)) 49 | for _, user := range users { 50 | go mail.SendMail([]string{user.Email}, templates.ServerDown(server), &wg) 51 | } 52 | return 53 | } 54 | } 55 | } 56 | } 57 | 58 | func PingAllServers() { 59 | db := database.OpenDatabase() 60 | 61 | defer db.Close() 62 | 63 | servers := db.GetServers() 64 | for _, server := range servers { 65 | PingServer(server) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/use_cases/sendMail/sendMail.go: -------------------------------------------------------------------------------- 1 | package sendMail 2 | 3 | import ( 4 | "sync" 5 | 6 | email "github.com/moohbr/WebMonitor/src/providers/mail" 7 | templates "github.com/moohbr/WebMonitor/src/providers/mail/templates" 8 | ) 9 | 10 | // Function to send the report mail 11 | func SendMail(To []string, template templates.Mail, wg *sync.WaitGroup) { 12 | mail := email.NewMail(To, template.Subject, template.Body) 13 | mail.Send() 14 | wg.Done() 15 | } 16 | --------------------------------------------------------------------------------