github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/notify/notify.go (about)

     1  package notify
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/mail"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/evergreen-ci/evergreen"
    14  	"github.com/evergreen-ci/evergreen/model"
    15  	"github.com/evergreen-ci/evergreen/model/build"
    16  	"github.com/evergreen-ci/evergreen/model/patch"
    17  	"github.com/evergreen-ci/evergreen/model/task"
    18  	"github.com/evergreen-ci/evergreen/model/user"
    19  	"github.com/evergreen-ci/evergreen/model/version"
    20  	"github.com/evergreen-ci/evergreen/util"
    21  	"github.com/evergreen-ci/evergreen/web"
    22  	"github.com/mongodb/grip"
    23  	"github.com/pkg/errors"
    24  	"gopkg.in/yaml.v2"
    25  )
    26  
    27  const (
    28  	// number of times to try sending a notification
    29  	NumSmtpRetries = 3
    30  
    31  	// period to sleep between tries
    32  	SmtpSleepTime = 1 * time.Second
    33  
    34  	// smtp port to connect to
    35  	SmtpPort = 25
    36  
    37  	// smtp relay host to connect to
    38  	SmtpServer = "localhost"
    39  
    40  	// Thus on each run of the notifier, we check if the last notification time
    41  	// (LNT) is within this window. If it is, then we use the retrieved LNT.
    42  	// If not, we use the current time
    43  	LNRWindow = 60 * time.Minute
    44  
    45  	// MCI ops notification prefaces
    46  	ProvisionFailurePreface = "[PROVISION-FAILURE]"
    47  	ProvisionTimeoutPreface = "[PROVISION-TIMEOUT]"
    48  	ProvisionLatePreface    = "[PROVISION-LATE]"
    49  	TeardownFailurePreface  = "[TEARDOWN-FAILURE]"
    50  
    51  	// repotracker notification prefaces
    52  	RepotrackerFailurePreface = "[REPOTRACKER-FAILURE %v] on %v"
    53  
    54  	// branch name to use if the project reference is not found
    55  	UnknownProjectBranch = ""
    56  
    57  	DefaultNotificationsConfig = "/etc/mci-notifications.yml"
    58  )
    59  
    60  var (
    61  	// notifications can be either build-level or task-level
    62  	// we might subsequently want to submit js test notifications
    63  	// within each task
    64  	buildType = "build"
    65  	taskType  = "task"
    66  
    67  	// notification key types
    68  	buildFailureKey          = "build_failure"
    69  	buildSuccessKey          = "build_success"
    70  	buildSuccessToFailureKey = "build_success_to_failure"
    71  	buildCompletionKey       = "build_completion"
    72  	taskFailureKey           = "task_failure"
    73  	taskSuccessKey           = "task_success"
    74  	taskSuccessToFailureKey  = "task_success_to_failure"
    75  	taskCompletionKey        = "task_completion"
    76  	taskFailureKeys          = []string{taskFailureKey, taskSuccessToFailureKey}
    77  	buildFailureKeys         = []string{buildFailureKey, buildSuccessToFailureKey}
    78  
    79  	// notification subjects
    80  	failureSubject    = "failed"
    81  	completionSubject = "completed"
    82  	successSubject    = "succeeded"
    83  	transitionSubject = "transitioned to failure"
    84  
    85  	// task/build status notification prefaces
    86  	mciSuccessPreface    = "[MCI-SUCCESS %v]"
    87  	mciFailurePreface    = "[MCI-FAILURE %v]"
    88  	mciCompletionPreface = "[MCI-COMPLETION %v]"
    89  
    90  	// patch notification prefaces
    91  	patchSuccessPreface    = "[PATCH-SUCCESS %v]"
    92  	patchFailurePreface    = "[PATCH-FAILURE %v]"
    93  	patchCompletionPreface = "[PATCH-COMPLETION %v]"
    94  
    95  	buildNotificationHandler = BuildNotificationHandler{buildType}
    96  	taskNotificationHandler  = TaskNotificationHandler{taskType}
    97  
    98  	Handlers = map[string]NotificationHandler{
    99  		buildFailureKey:          &BuildFailureHandler{buildNotificationHandler, buildFailureKey},
   100  		buildSuccessKey:          &BuildSuccessHandler{buildNotificationHandler, buildSuccessKey},
   101  		buildCompletionKey:       &BuildCompletionHandler{buildNotificationHandler, buildCompletionKey},
   102  		buildSuccessToFailureKey: &BuildSuccessToFailureHandler{buildNotificationHandler, buildSuccessToFailureKey},
   103  		taskFailureKey:           &TaskFailureHandler{taskNotificationHandler, taskFailureKey},
   104  		taskSuccessKey:           &TaskSuccessHandler{taskNotificationHandler, taskSuccessKey},
   105  		taskCompletionKey:        &TaskCompletionHandler{taskNotificationHandler, taskCompletionKey},
   106  		taskSuccessToFailureKey:  &TaskSuccessToFailureHandler{taskNotificationHandler, taskSuccessToFailureKey},
   107  	}
   108  )
   109  
   110  var (
   111  	// These help us to limit the tasks/builds we pull
   112  	// from the database on each run of the notifier
   113  	lastProjectNotificationTime = make(map[string]time.Time)
   114  	newProjectNotificationTime  = make(map[string]time.Time)
   115  
   116  	// Simple map for optimization
   117  	cachedProjectRecords = make(map[string]interface{})
   118  
   119  	// Simple map for caching project refs
   120  	cachedProjectRef = make(map[string]*model.ProjectRef)
   121  )
   122  
   123  func ConstructMailer(notifyConfig evergreen.NotifyConfig) Mailer {
   124  	if notifyConfig.SMTP != nil {
   125  		return SmtpMailer{
   126  			notifyConfig.SMTP.From,
   127  			notifyConfig.SMTP.Server,
   128  			notifyConfig.SMTP.Port,
   129  			notifyConfig.SMTP.UseSSL,
   130  			notifyConfig.SMTP.Username,
   131  			notifyConfig.SMTP.Password,
   132  		}
   133  	} else {
   134  		return SmtpMailer{
   135  			Server: SmtpServer,
   136  			Port:   SmtpPort,
   137  			UseSSL: false,
   138  		}
   139  	}
   140  }
   141  
   142  // This function is responsible for running the notifications pipeline
   143  //
   144  // ParseNotifications
   145  //         ↓↓
   146  // ValidateNotifications
   147  //         ↓↓
   148  // ProcessNotifications
   149  //         ↓↓
   150  // SendNotifications
   151  //         ↓↓
   152  // UpdateNotificationTimes
   153  //
   154  func Run(settings *evergreen.Settings) error {
   155  	// get the notifications
   156  	mciNotification, err := ParseNotifications(settings.ConfigDir)
   157  	if err != nil {
   158  		grip.Errorf("parsing notifications: %+v", err)
   159  		return err
   160  	}
   161  
   162  	// validate the notifications
   163  	err = ValidateNotifications(mciNotification)
   164  	if err != nil {
   165  		grip.Errorf("validating notifications: %+v", err)
   166  		return err
   167  	}
   168  
   169  	templateGlobals := map[string]interface{}{
   170  		"UIRoot": settings.Ui.Url,
   171  	}
   172  
   173  	ae, err := createEnvironment(settings, templateGlobals)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	// process the notifications
   179  	emails, err := ProcessNotifications(ae, mciNotification, true)
   180  	if err != nil {
   181  		grip.Errorf("processing notifications: %+v", err)
   182  		return err
   183  	}
   184  
   185  	// Remnants are outstanding build/task notifications that
   186  	// we couldn't process on a prior run of the notifier
   187  	// Currently, this can only happen if an administrator
   188  	// bumps up the priority of a build/task
   189  
   190  	// send the notifications
   191  
   192  	err = SendNotifications(settings, mciNotification, emails,
   193  		ConstructMailer(settings.Notify))
   194  	if err != nil {
   195  		grip.Errorln("Error sending notifications:", err)
   196  		return err
   197  	}
   198  
   199  	// update notification times
   200  	err = UpdateNotificationTimes()
   201  	if err != nil {
   202  		grip.Errorln("Error updating notification times:", err)
   203  		return err
   204  	}
   205  	return nil
   206  }
   207  
   208  // This function is responsible for reading the notifications file
   209  func ParseNotifications(configName string) (*MCINotification, error) {
   210  	grip.Info("Parsing notifications...")
   211  
   212  	evgHome := evergreen.FindEvergreenHome()
   213  	configs := []string{
   214  		filepath.Join(evgHome, configName, evergreen.NotificationsFile),
   215  		filepath.Join(evgHome, evergreen.NotificationsFile),
   216  		DefaultNotificationsConfig,
   217  	}
   218  
   219  	var notificationsFile string
   220  	for _, fn := range configs {
   221  		if _, err := os.Stat(fn); os.IsNotExist(err) {
   222  			continue
   223  		}
   224  
   225  		notificationsFile = fn
   226  	}
   227  
   228  	data, err := ioutil.ReadFile(notificationsFile)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	// unmarshal file contents into MCINotification struct
   234  	mciNotification := &MCINotification{}
   235  
   236  	err = yaml.Unmarshal(data, mciNotification)
   237  	if err != nil {
   238  		return nil, errors.Wrapf(err, "Parse error unmarshalling notifications %v", notificationsFile)
   239  	}
   240  	return mciNotification, nil
   241  }
   242  
   243  // This function is responsible for validating the notifications file
   244  func ValidateNotifications(mciNotification *MCINotification) error {
   245  	grip.Info("Validating notifications...")
   246  	allNotifications := []string{}
   247  
   248  	projectNameToBuildVariants, err := findProjectBuildVariants()
   249  	if err != nil {
   250  		return errors.Wrap(err, "Error loading project build variants")
   251  	}
   252  
   253  	// Validate default notification recipients
   254  	for _, notification := range mciNotification.Notifications {
   255  		if notification.Project == "" {
   256  			return errors.Errorf("Must specify a project for each notification - see %v", notification.Name)
   257  		}
   258  
   259  		buildVariants, ok := projectNameToBuildVariants[notification.Project]
   260  		if !ok {
   261  			return errors.Errorf("Notifications validation failed: "+
   262  				"project `%v` not found", notification.Project)
   263  		}
   264  
   265  		// ensure all supplied build variants are valid
   266  		for _, buildVariant := range notification.SkipVariants {
   267  			if !util.SliceContains(buildVariants, buildVariant) {
   268  				return errors.Errorf("Nonexistent buildvariant - ”%v” - specified for ”%v” notification", buildVariant, notification.Name)
   269  			}
   270  		}
   271  
   272  		allNotifications = append(allNotifications, notification.Name)
   273  	}
   274  
   275  	// Validate team name and addresses
   276  	for _, team := range mciNotification.Teams {
   277  		if team.Name == "" {
   278  			return errors.Errorf("Each notification team must have a name")
   279  		}
   280  
   281  		for _, subscription := range team.Subscriptions {
   282  			for _, notification := range subscription.NotifyOn {
   283  				if !util.SliceContains(allNotifications, notification) {
   284  					return errors.Errorf("Team ”%v” contains a non-existent subscription - %v", team.Name, notification)
   285  				}
   286  			}
   287  			for _, buildVariant := range subscription.SkipVariants {
   288  				buildVariants, ok := projectNameToBuildVariants[subscription.Project]
   289  				if !ok {
   290  					return errors.Errorf("Teams validation failed: project `%v` not found", subscription.Project)
   291  				}
   292  
   293  				if !util.SliceContains(buildVariants, buildVariant) {
   294  					return errors.Errorf("Nonexistent buildvariant - ”%v” - specified for team ”%v” ", buildVariant, team.Name)
   295  				}
   296  			}
   297  		}
   298  	}
   299  
   300  	// Validate patch notifications
   301  	for _, subscription := range mciNotification.PatchNotifications {
   302  		for _, notification := range subscription.NotifyOn {
   303  			if !util.SliceContains(allNotifications, notification) {
   304  				return errors.Errorf("Nonexistent patch notification - ”%v” - specified", notification)
   305  			}
   306  		}
   307  	}
   308  
   309  	// validate the patch notification buildvariatns
   310  	for _, subscription := range mciNotification.PatchNotifications {
   311  		buildVariants, ok := projectNameToBuildVariants[subscription.Project]
   312  		if !ok {
   313  			return errors.Errorf("Patch notification build variants validation failed: "+
   314  				"project `%v` not found", subscription.Project)
   315  		}
   316  
   317  		for _, buildVariant := range subscription.SkipVariants {
   318  			if !util.SliceContains(buildVariants, buildVariant) {
   319  				return errors.Errorf("Nonexistent buildvariant - ”%v” - specified for patch notifications", buildVariant)
   320  			}
   321  		}
   322  	}
   323  
   324  	// all good!
   325  	return nil
   326  }
   327  
   328  // This function is responsible for all notifications processing
   329  func ProcessNotifications(ae *web.App, mciNotification *MCINotification, updateTimes bool) (map[NotificationKey][]Email, error) {
   330  	// create MCI notifications
   331  	allNotificationsSlice := notificationsToStruct(mciNotification)
   332  
   333  	// get the last notification time for all projects
   334  	if updateTimes {
   335  		err := getLastProjectNotificationTime(allNotificationsSlice)
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  	}
   340  
   341  	grip.Info("Processing notifications...")
   342  
   343  	emails := make(map[NotificationKey][]Email)
   344  	for _, key := range allNotificationsSlice {
   345  		emailsForKey, err := Handlers[key.NotificationName].GetNotifications(ae, &key)
   346  		if err != nil {
   347  			grip.Infof("Error processing %s on %s: %+v", key.NotificationName, key.Project, err)
   348  			continue
   349  		}
   350  		emails[key] = emailsForKey
   351  	}
   352  
   353  	return emails, nil
   354  }
   355  
   356  // This function is responsible for managing the sending triggered email notifications
   357  func SendNotifications(settings *evergreen.Settings, mciNotification *MCINotification,
   358  	emails map[NotificationKey][]Email, mailer Mailer) (err error) {
   359  	grip.Info("Sending notifications...")
   360  
   361  	// parse all notifications, sending it to relevant recipients
   362  	for _, notification := range mciNotification.Notifications {
   363  		key := NotificationKey{
   364  			Project:               notification.Project,
   365  			NotificationName:      notification.Name,
   366  			NotificationType:      getType(notification.Name),
   367  			NotificationRequester: evergreen.RepotrackerVersionRequester,
   368  		}
   369  
   370  		for _, recipient := range notification.Recipients {
   371  			// send all triggered notifications
   372  			for _, email := range emails[key] {
   373  
   374  				// determine if this notification should be skipped - based on the buildvariant
   375  				if email.ShouldSkip(notification.SkipVariants) {
   376  					continue
   377  				}
   378  
   379  				// send to individual subscriber, or the admin team if it's not their fault
   380  				recipients := []string{}
   381  				if settings.Notify.SMTP != nil {
   382  					recipients = settings.Notify.SMTP.AdminEmail
   383  				}
   384  				if !email.IsLikelySystemFailure() {
   385  					recipients = email.GetRecipients(recipient)
   386  				}
   387  				err = TrySendNotification(recipients, email.GetSubject(), email.GetBody(), mailer)
   388  				if err != nil {
   389  					grip.Errorf("Unable to send individual notification %#v: %+v", key, err)
   390  					continue
   391  				}
   392  			}
   393  		}
   394  	}
   395  
   396  	// Send team subscribed notifications
   397  	for _, team := range mciNotification.Teams {
   398  		for _, subscription := range team.Subscriptions {
   399  			for _, name := range subscription.NotifyOn {
   400  				key := NotificationKey{
   401  					Project:               subscription.Project,
   402  					NotificationName:      name,
   403  					NotificationType:      getType(name),
   404  					NotificationRequester: evergreen.RepotrackerVersionRequester,
   405  				}
   406  
   407  				// send all triggered notifications for this key
   408  				for _, email := range emails[key] {
   409  					// determine if this notification should be skipped - based on the buildvariant
   410  					if email.ShouldSkip(subscription.SkipVariants) {
   411  						continue
   412  					}
   413  
   414  					teamEmail := fmt.Sprintf("%v <%v>", team.Name, team.Address)
   415  					err = TrySendNotification([]string{teamEmail}, email.GetSubject(), email.GetBody(), mailer)
   416  					if err != nil {
   417  						grip.Errorf("Unable to send notification %#v: %v", key, err)
   418  						continue
   419  					}
   420  				}
   421  			}
   422  		}
   423  	}
   424  
   425  	// send patch notifications
   426  	/* XXX temporarily disable patch notifications
   427  	for _, subscription := range mciNotification.PatchNotifications {
   428  		for _, notification := range subscription.NotifyOn {
   429  			key := NotificationKey{
   430  				Project:               subscription.Project,
   431  				NotificationName:      notification,
   432  				NotificationType:      getType(notification),
   433  				NotificationRequester: evergreen.PatchVersionRequester,
   434  			}
   435  
   436  			for _, email := range emails[key] {
   437  				// determine if this notification should be skipped -
   438  				// based on the buildvariant
   439  				if email.ShouldSkip(subscription.SkipVariants) {
   440  					continue
   441  				}
   442  
   443  				// send to the appropriate patch requester
   444  				for _, changeInfo := range email.GetChangeInfo() {
   445  					// send notification to each member of the blamelist
   446  					patchRequester := fmt.Sprintf("%v <%v>", changeInfo.Author,
   447  						changeInfo.Email)
   448  					err = TrySendNotification([]string{patchRequester},
   449  						email.GetSubject(), email.GetBody(), mailer)
   450  					if err != nil {
   451  						grip.Errorf("Unable to send notification %#v: %+v", key, err)
   452  						continue
   453  					}
   454  				}
   455  			}
   456  		}
   457  	}*/
   458  
   459  	return nil
   460  }
   461  
   462  // This stores the last time threshold after which
   463  // we search for possible new notification events
   464  func UpdateNotificationTimes() (err error) {
   465  	grip.Info("Updating notification times...")
   466  	for project, time := range newProjectNotificationTime {
   467  		grip.Infof("Updating %s notification time...", project)
   468  		err = model.SetLastNotificationsEventTime(project, time)
   469  		if err != nil {
   470  			return err
   471  		}
   472  	}
   473  	return nil
   474  }
   475  
   476  //***********************************\/
   477  //   Notification Helper Functions   \/
   478  //***********************************\/
   479  
   480  // Construct a map of project names to build variants for that project
   481  func findProjectBuildVariants() (map[string][]string, error) {
   482  	projectNameToBuildVariants := make(map[string][]string)
   483  
   484  	allProjects, err := model.FindAllTrackedProjectRefs()
   485  	if err != nil {
   486  		return nil, err
   487  	}
   488  
   489  	for _, projectRef := range allProjects {
   490  		if !projectRef.Enabled {
   491  			continue
   492  		}
   493  		var buildVariants []string
   494  		var proj *model.Project
   495  		var err error
   496  		if projectRef.LocalConfig != "" {
   497  			proj, err = model.FindProject("", &projectRef)
   498  			if err != nil {
   499  				return nil, errors.Wrap(err, "unable to find project file")
   500  			}
   501  		} else {
   502  			lastGood, err := version.FindOne(version.ByLastKnownGoodConfig(projectRef.Identifier))
   503  			if err != nil {
   504  				return nil, errors.Wrap(err, "unable to find last valid config")
   505  			}
   506  			if lastGood == nil { // brand new project + no valid config yet, just return an empty map
   507  				return projectNameToBuildVariants, nil
   508  			}
   509  
   510  			proj = &model.Project{}
   511  			err = model.LoadProjectInto([]byte(lastGood.Config), projectRef.Identifier, proj)
   512  			if err != nil {
   513  				return nil, errors.Wrapf(err, "error loading project '%v' from version",
   514  					projectRef.Identifier)
   515  			}
   516  		}
   517  
   518  		for _, buildVariant := range proj.BuildVariants {
   519  			buildVariants = append(buildVariants, buildVariant.Name)
   520  		}
   521  
   522  		projectNameToBuildVariants[projectRef.Identifier] = buildVariants
   523  	}
   524  
   525  	return projectNameToBuildVariants, nil
   526  }
   527  
   528  // construct the change information
   529  // struct from a given version struct
   530  func constructChangeInfo(v *version.Version, notification *NotificationKey) (changeInfo *ChangeInfo) {
   531  	changeInfo = &ChangeInfo{}
   532  	switch notification.NotificationRequester {
   533  	case evergreen.RepotrackerVersionRequester:
   534  		changeInfo.Project = v.Identifier
   535  		changeInfo.Author = v.Author
   536  		changeInfo.Message = v.Message
   537  		changeInfo.Revision = v.Revision
   538  		changeInfo.Email = v.AuthorEmail
   539  
   540  	case evergreen.PatchVersionRequester:
   541  		// get the author and description from the patch request
   542  		patch, err := patch.FindOne(patch.ByVersion(v.Id))
   543  		if err != nil {
   544  			grip.Errorf("Error finding patch for version %s: %+v", v.Id, err)
   545  			return
   546  		}
   547  
   548  		if patch == nil {
   549  			grip.Errorln(notification, "notification was unable to locate patch with version:", v.Id)
   550  			return
   551  		}
   552  		// get the display name and email for this user
   553  		dbUser, err := user.FindOne(user.ById(patch.Author))
   554  		if err != nil {
   555  			grip.Errorf("Error finding user %s: %+v", patch.Author, err)
   556  			changeInfo.Author = patch.Author
   557  			changeInfo.Email = patch.Author
   558  		} else if dbUser == nil {
   559  			grip.Errorf("User %s not found", patch.Author)
   560  			changeInfo.Author = patch.Author
   561  			changeInfo.Email = patch.Author
   562  		} else {
   563  			changeInfo.Email = dbUser.Email()
   564  			changeInfo.Author = dbUser.DisplayName()
   565  		}
   566  
   567  		changeInfo.Project = patch.Project
   568  		changeInfo.Message = patch.Description
   569  		changeInfo.Revision = patch.Id.Hex()
   570  	}
   571  	return
   572  }
   573  
   574  // use mail's rfc2047 to encode any string
   575  func encodeRFC2047(String string) string {
   576  	addr := mail.Address{
   577  		Name:    String,
   578  		Address: "",
   579  	}
   580  	return strings.Trim(addr.String(), " <>")
   581  }
   582  
   583  // get the display name for a build variant's
   584  // task given the build variant name
   585  func getDisplayName(buildVariant string) (displayName string) {
   586  	build, err := build.FindOne(build.ByVariant(buildVariant))
   587  	if err != nil || build == nil {
   588  		grip.Error(errors.Wrap(err, "Error fetching buildvariant name"))
   589  		displayName = buildVariant
   590  	} else {
   591  		displayName = build.DisplayName
   592  	}
   593  	return
   594  }
   595  
   596  // get the failed task(s) for a given build
   597  func getFailedTasks(current *build.Build, notificationName string) (failedTasks []build.TaskCache) {
   598  	if util.SliceContains(buildFailureKeys, notificationName) {
   599  		for _, t := range current.Tasks {
   600  			if t.Status == evergreen.TaskFailed {
   601  				failedTasks = append(failedTasks, t)
   602  			}
   603  		}
   604  	}
   605  	return
   606  }
   607  
   608  // get the specific failed test(s) for this task
   609  func getFailedTests(current *task.Task, notificationName string) (failedTests []task.TestResult) {
   610  	if util.SliceContains(taskFailureKeys, notificationName) {
   611  		for _, test := range current.TestResults {
   612  			if test.Status == "fail" {
   613  				// get the base name for windows/non-windows paths
   614  				test.TestFile = path.Base(strings.Replace(test.TestFile, "\\", "/", -1))
   615  				failedTests = append(failedTests, test)
   616  			}
   617  		}
   618  	}
   619  
   620  	return
   621  }
   622  
   623  // gets the project ref project name corresponding to this identifier
   624  func getProjectRef(identifier string) (projectRef *model.ProjectRef,
   625  	err error) {
   626  	if cachedProjectRef[identifier] == nil {
   627  		projectRef, err = model.FindOneProjectRef(identifier)
   628  		if err != nil {
   629  			return
   630  		}
   631  		cachedProjectRecords[identifier] = projectRef
   632  		return projectRef, nil
   633  	}
   634  	return cachedProjectRef[identifier], nil
   635  }
   636  
   637  // This gets the time threshold - events before which
   638  // we searched for possible notification events
   639  func getLastProjectNotificationTime(keys []NotificationKey) error {
   640  	for _, key := range keys {
   641  		lastNotificationTime, err := model.LastNotificationsEventTime(key.Project)
   642  		if err != nil {
   643  			return errors.WithStack(err)
   644  		}
   645  		if lastNotificationTime.Before(time.Now().Add(-LNRWindow)) {
   646  			lastNotificationTime = time.Now()
   647  		}
   648  		lastProjectNotificationTime[key.Project] = lastNotificationTime
   649  		newProjectNotificationTime[key.Project] = time.Now()
   650  	}
   651  	return nil
   652  }
   653  
   654  // This is used to pull recently finished builds
   655  func getRecentlyFinishedBuilds(notificationKey *NotificationKey) (builds []build.Build, err error) {
   656  	if cachedProjectRecords[notificationKey.String()] == nil {
   657  		builds, err = build.Find(build.ByFinishedAfter(lastProjectNotificationTime[notificationKey.Project], notificationKey.Project, notificationKey.NotificationRequester))
   658  		if err != nil {
   659  			return nil, errors.WithStack(err)
   660  		}
   661  		cachedProjectRecords[notificationKey.String()] = builds
   662  		return builds, errors.WithStack(err)
   663  	}
   664  	return cachedProjectRecords[notificationKey.String()].([]build.Build), nil
   665  }
   666  
   667  // This is used to pull recently finished tasks
   668  func getRecentlyFinishedTasks(notificationKey *NotificationKey) (tasks []task.Task, err error) {
   669  	if cachedProjectRecords[notificationKey.String()] == nil {
   670  
   671  		tasks, err = task.Find(task.ByRecentlyFinished(lastProjectNotificationTime[notificationKey.Project],
   672  			notificationKey.Project, notificationKey.NotificationRequester))
   673  		if err != nil {
   674  			return nil, errors.WithStack(err)
   675  		}
   676  		cachedProjectRecords[notificationKey.String()] = tasks
   677  		return tasks, errors.WithStack(err)
   678  	}
   679  	return cachedProjectRecords[notificationKey.String()].([]task.Task), nil
   680  }
   681  
   682  // gets the type of notification - we support build/task level notification
   683  func getType(notification string) (nkType string) {
   684  	nkType = taskType
   685  	if strings.Contains(notification, buildType) {
   686  		nkType = buildType
   687  	}
   688  	return
   689  }
   690  
   691  // creates/returns slice of 'relevant' NotificationKeys a
   692  // notification is relevant if it has at least one recipient
   693  func notificationsToStruct(mciNotification *MCINotification) (notifyOn []NotificationKey) {
   694  	// Get default notifications
   695  	for _, notification := range mciNotification.Notifications {
   696  		if len(notification.Recipients) != 0 {
   697  			// flag the notification as needed
   698  			key := NotificationKey{
   699  				Project:               notification.Project,
   700  				NotificationName:      notification.Name,
   701  				NotificationType:      getType(notification.Name),
   702  				NotificationRequester: evergreen.RepotrackerVersionRequester,
   703  			}
   704  
   705  			// prevent duplicate notifications from being sent
   706  			if !util.SliceContains(notifyOn, key) {
   707  				notifyOn = append(notifyOn, key)
   708  			}
   709  		}
   710  	}
   711  
   712  	// Get team notifications
   713  	for _, team := range mciNotification.Teams {
   714  		for _, subscription := range team.Subscriptions {
   715  			for _, name := range subscription.NotifyOn {
   716  				key := NotificationKey{
   717  					Project:               subscription.Project,
   718  					NotificationName:      name,
   719  					NotificationType:      getType(name),
   720  					NotificationRequester: evergreen.RepotrackerVersionRequester,
   721  				}
   722  
   723  				// prevent duplicate notifications from being sent
   724  				if !util.SliceContains(notifyOn, key) {
   725  					notifyOn = append(notifyOn, key)
   726  				}
   727  			}
   728  		}
   729  	}
   730  
   731  	// Get patch notifications
   732  	for _, subscription := range mciNotification.PatchNotifications {
   733  		for _, notification := range subscription.NotifyOn {
   734  			key := NotificationKey{
   735  				Project:               subscription.Project,
   736  				NotificationName:      notification,
   737  				NotificationType:      getType(notification),
   738  				NotificationRequester: evergreen.PatchVersionRequester,
   739  			}
   740  
   741  			// prevent duplicate notifications from being sent
   742  			if !util.SliceContains(notifyOn, key) {
   743  				notifyOn = append(notifyOn, key)
   744  			}
   745  		}
   746  	}
   747  	return
   748  }
   749  
   750  // NotifyAdmins is a helper method to send a notification to the MCI admin team
   751  func NotifyAdmins(subject, message string, settings *evergreen.Settings) error {
   752  	if settings.Notify.SMTP != nil {
   753  		return TrySendNotification(settings.Notify.SMTP.AdminEmail,
   754  			subject, message, ConstructMailer(settings.Notify))
   755  	}
   756  	err := errors.New("Cannot notify admins: admin_email not set")
   757  	grip.Error(err)
   758  	return err
   759  }
   760  
   761  // String method for notification key
   762  func (nk NotificationKey) String() string {
   763  	return fmt.Sprintf("%v-%v-%v", nk.Project, nk.NotificationType, nk.NotificationRequester)
   764  }
   765  
   766  // Helper function to send notifications
   767  func TrySendNotification(recipients []string, subject, body string, mailer Mailer) (err error) {
   768  	// grip.Debugf("address: %s subject: %s body: %s", recipients, subject, body)
   769  	// return nil
   770  	_, err = util.Retry(func() error {
   771  		err = mailer.SendMail(recipients, subject, body)
   772  		if err != nil {
   773  			grip.Errorln("Error sending notification:", err)
   774  			return util.RetriableError{err}
   775  		}
   776  		return nil
   777  	}, NumSmtpRetries, SmtpSleepTime)
   778  	return errors.WithStack(err)
   779  }
   780  
   781  // Helper function to send notification to a given user
   782  func TrySendNotificationToUser(userId string, subject, body string, mailer Mailer) error {
   783  	dbUser, err := user.FindOne(user.ById(userId))
   784  	if err != nil {
   785  		return errors.Wrapf(err, "Error finding user %v", userId)
   786  	} else if dbUser == nil {
   787  		return errors.Errorf("User %v not found", userId)
   788  	} else {
   789  		return errors.WithStack(TrySendNotification([]string{dbUser.Email()}, subject, body, mailer))
   790  	}
   791  }
   792  
   793  //***************************\/
   794  //   Notification Structs    \/
   795  //***************************\/
   796  
   797  // stores supported notifications
   798  type Notification struct {
   799  	Name         string   `yaml:"name"`
   800  	Project      string   `yaml:"project"`
   801  	Recipients   []string `yaml:"recipients"`
   802  	SkipVariants []string `yaml:"skip_variants"`
   803  }
   804  
   805  // stores notifications subscription for a team
   806  type Subscription struct {
   807  	Project      string   `yaml:"project"`
   808  	SkipVariants []string `yaml:"skip_variants"`
   809  	NotifyOn     []string `yaml:"notify_on"`
   810  }
   811  
   812  // stores 10gen team information
   813  type Team struct {
   814  	Name          string         `yaml:"name"`
   815  	Address       string         `yaml:"address"`
   816  	Subscriptions []Subscription `yaml:"subscriptions"`
   817  }
   818  
   819  // store notifications file
   820  type MCINotification struct {
   821  	Notifications      []Notification `yaml:"notifications"`
   822  	Teams              []Team         `yaml:"teams"`
   823  	PatchNotifications []Subscription `yaml:"patch_notifications"`
   824  }
   825  
   826  // stores high level notifications key
   827  type NotificationKey struct {
   828  	Project               string
   829  	NotificationName      string
   830  	NotificationType      string
   831  	NotificationRequester string
   832  }
   833  
   834  // stores information pertaining to a repository's changes
   835  type ChangeInfo struct {
   836  	Revision string
   837  	Author   string
   838  	Email    string
   839  	Pushtime string
   840  	Project  string
   841  	Message  string
   842  }