public void Send(RosterSummary roster)
        {
            var(address, port, username, password, templateFilename) = EmailServerDetailsFromEnvironment();
            Console.WriteLine($"Using template file {templateFilename}");
            var message = new MimeMessage();

            message.From.Add(new MailboxAddress("Student IT and eLearning Support", "*****@*****.**"));

            foreach (var emailAddress in RecipientEmails)
            {
                message.To.Add(new MailboxAddress(emailAddress));
            }
            // Take 1 day off the enddate as it's configured to search up to midnight on the last day, which means effectively
            // the roster is up to the end of the day before. Display like this for clarity
            message.Subject = $"Shift Summary ({roster.StartDate.ToString("yyyy-MM-dd")} - {roster.EndDate.AddDays(-1).ToString("yyyy-MM-dd")})";


            var      tableHeader = new List <string>();
            DateTime curDate     = roster.StartDate;

            while (curDate < roster.EndDate)
            {
                tableHeader.Add(curDate.ToString("dd-MM"));
                curDate = curDate.AddDays(1);
            }

            roster.Employees.Sort((a, b) => a.Name.CompareTo(b.Name));
            var model = new
            {
                Employees = roster.Employees,
                Header    = tableHeader
            };

            Template.RegisterSafeType(typeof(Employee), new[] { "Name", "TotalHours", "Shifts" });
            Template.RegisterFilter(typeof(HoursFilter));
            Template.RegisterFilter(typeof(ShiftStyleFilter));
            Template template = Template.Parse(File.ReadAllText(Path.Combine("extra", templateFilename)));
            var      bodyText = template.Render(Hash.FromAnonymousObject(model));

            message.Body = new TextPart("html")
            {
                Text = bodyText
            };

            using (var client = new SmtpClient())
            {
                Console.WriteLine("Sending email");

                client.Timeout = 10000;
                client.Connect(address, port);

                client.AuthenticationMechanisms.Remove("XOAUTH2");
                client.Authenticate(username, password);

                client.Send(message);
                client.Disconnect(true);
            }
        }
        private Employee EmployeeByName(string name, RosterSummary rosterSummary)
        {
            Employee employeeWithName = null;

            foreach (Employee employee in rosterSummary.Employees)
            {
                if (employee.Name == name)
                {
                    employeeWithName = employee;
                }
            }

            if (employeeWithName == null)
            {
                employeeWithName = new Employee(name, rosterSummary.Days);
                rosterSummary.Employees.Add(employeeWithName);
            }

            return(employeeWithName);
        }
        public void MakeSummary()
        {
            CultureInfo.DefaultThreadCurrentCulture   = CultureInfo.InvariantCulture;
            CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;

            Console.WriteLine("Getting roster bounds");
            var(periodStart, periodEnd, shouldRun) = GetRosterBounds();

            if (!shouldRun)
            {
                // Workaround the fact that you cannot schedule a lambda to run every 2 weeks
                Console.WriteLine("Cancelling summary as it's not the end of a pay period");
                return;
            }

            Console.WriteLine($"Loading summary for period {periodStart} - {periodEnd}");

            RosterSummary rosterSummary = new RosterSummary(periodStart, periodEnd);

            var service        = GcalProvider.MakeService();
            var internProvider = new InternProvider();

            // Define parameters of request.
            foreach (var calendarId in _calendars)
            {
                var request = service.Events.List(calendarId);
                request.TimeMin      = periodStart;
                request.TimeMax      = periodEnd;
                request.ShowDeleted  = false;
                request.SingleEvents = true;
                request.MaxResults   = 2500;
                request.OrderBy      = EventsResource.ListRequest.OrderByEnum.StartTime;

                Events events = request.Execute();
                if (events.Items != null)
                {
                    Console.WriteLine($"Got {events.Items.Count} events from {calendarId}");

                    foreach (var eventItem in events.Items)
                    {
                        // Location will be a string of either BEE/Baillieu/ERC
                        // It's irrelevant for hours calculation but helpful for logging
                        var startTime = eventItem.Start.DateTime;
                        var endTime   = eventItem.End.DateTime;
                        var location  = eventItem.Location;

                        if (eventItem.Status == "cancelled")
                        {
                            // Ignore cancelled events since it means the shift was not actully worked
                            Console.WriteLine($"Skipping cancelled event {startTime} - {endTime} @ {location}");
                            continue;
                        }
                        if (eventItem.Attendees.Count == 0)
                        {
                            // This event is probably a valid shift, but we have no way of knowing
                            // Who owns it. Log it and continue
                            Console.WriteLine($"Skipping event with 0 attendees {startTime} - {endTime} @ {location}");
                            continue;
                        }
                        if (startTime < rosterSummary.StartDate || endTime > rosterSummary.EndDate)
                        {
                            Console.WriteLine($"Skipping event which is outside of roster timeframe {startTime} - {endTime} @ {location}");
                        }

                        var internEmail = eventItem.Attendees[0].Email;
                        if (eventItem.Attendees.Count > 1)
                        {
                            // People sometimes add personal and work emails to shifts. This shouldn't
                            // be a problem, but good to log it just in case
                            Console.WriteLine($"Found event with multiple attendees {startTime}-{endTime} @ {location}. Using {internEmail}");
                        }

                        var internName = internProvider.NameFromEmail(internEmail);

                        var      employee = EmployeeByName(internName ?? internEmail, rosterSummary);
                        var      hours    = (endTime - startTime).Value.TotalHours;
                        DateTime curDate  = rosterSummary.StartDate;
                        for (var i = 0; i < rosterSummary.Days; i++)
                        {
                            if (curDate.Date == endTime.Value.Date)
                            {
                                employee.Shifts[i] += hours;
                                break;
                            }
                            curDate = curDate.AddDays(1);
                        }
                    }
                }
                else
                {
                    Console.WriteLine($"Got no events from {calendarId}. Is the selected time period correct? {request.TimeMin}-{request.TimeMax}");
                }
            }

            var report = new EmailReport();

            report.Send(rosterSummary);
        }