Adding "Report Email" Support (#1014)

Adds the capability to report phishing campaigns using an email client extension.

**Note: Gophish does not currently provide an email client extension out of the box. This is simply a mechanism to let existing email client add-ons send report status information to Gophish, and have that information reflected in the dashboard.**
pull/1003/merge
Jordan Wright 2018-03-18 22:03:00 -05:00 committed by GitHub
parent 709e83bade
commit f21536da7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 209 additions and 52 deletions

View File

@ -39,6 +39,8 @@ func CreatePhishingRouter() http.Handler {
router.HandleFunc("/track", PhishTracker) router.HandleFunc("/track", PhishTracker)
router.HandleFunc("/robots.txt", RobotsHandler) router.HandleFunc("/robots.txt", RobotsHandler)
router.HandleFunc("/{path:.*}/track", PhishTracker) router.HandleFunc("/{path:.*}/track", PhishTracker)
router.HandleFunc("/{path:.*}/report", PhishReporter)
router.HandleFunc("/report", PhishReporter)
router.HandleFunc("/{path:.*}", PhishHandler) router.HandleFunc("/{path:.*}", PhishHandler)
return router return router
} }
@ -71,6 +73,29 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/images/pixel.png") http.ServeFile(w, r, "static/images/pixel.png")
} }
// PhishReporter tracks emails as they are reported, updating the status for the given Result
func PhishReporter(w http.ResponseWriter, r *http.Request) {
err, r := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
if err != ErrInvalidRequest && err != ErrCampaignComplete {
Logger.Println(err)
}
http.NotFound(w, r)
return
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_REPORTED, Details: string(rj)})
err = rs.UpdateReported(true)
if err != nil {
Logger.Println(err)
}
w.WriteHeader(http.StatusNoContent)
}
// PhishHandler handles incoming client connections and registers the associated actions performed // PhishHandler handles incoming client connections and registers the associated actions performed
// (such as clicked link, etc.) // (such as clicked link, etc.)
func PhishHandler(w http.ResponseWriter, r *http.Request) { func PhishHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -26,6 +26,12 @@ func (s *ControllersSuite) openEmail(rid string) {
s.Equal(bytes.Compare(body, expected), 0) s.Equal(bytes.Compare(body, expected), 0)
} }
func (s *ControllersSuite) reportedEmail(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ps.URL, models.RecipientParameter, rid))
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusNoContent)
}
func (s *ControllersSuite) openEmail404(rid string) { func (s *ControllersSuite) openEmail404(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid)) resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
s.Nil(err) s.Nil(err)
@ -63,6 +69,19 @@ func (s *ControllersSuite) TestOpenedPhishingEmail() {
s.Equal(result.Status, models.EVENT_OPENED) s.Equal(result.Status, models.EVENT_OPENED)
} }
func (s *ControllersSuite) TestReportedPhishingEmail() {
campaign := s.getFirstCampaign()
result := campaign.Results[0]
s.Equal(result.Status, models.STATUS_SENDING)
s.reportedEmail(result.RId)
campaign = s.getFirstCampaign()
result = campaign.Results[0]
s.Equal(result.Reported, true)
s.Equal(campaign.Events[len(campaign.Events)-1].Message, models.EVENT_REPORTED)
}
func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() { func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
campaign := s.getFirstCampaign() campaign := s.getFirstCampaign()
result := campaign.Results[0] result := campaign.Results[0]

View File

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN reported boolean default 0;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN reported boolean default 0;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -33,6 +33,7 @@ type CampaignResults struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status string `json:"status"`
Reported string `json:"reported"`
Results []Result `json:"results, omitempty"` Results []Result `json:"results, omitempty"`
Events []Event `json:"timeline,omitempty"` Events []Event `json:"timeline,omitempty"`
} }
@ -61,6 +62,7 @@ type CampaignStats struct {
OpenedEmail int64 `json:"opened"` OpenedEmail int64 `json:"opened"`
ClickedLink int64 `json:"clicked"` ClickedLink int64 `json:"clicked"`
SubmittedData int64 `json:"submitted_data"` SubmittedData int64 `json:"submitted_data"`
EmailReported int64 `json:"email_reported"`
Error int64 `json:"error"` Error int64 `json:"error"`
} }
@ -194,6 +196,10 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
if err != nil { if err != nil {
return s, err return s, err
} }
query.Where("reported=?", true).Count(&s.EmailReported)
if err != nil {
return s, err
}
// Every submitted data event implies they clicked the link // Every submitted data event implies they clicked the link
s.ClickedLink += s.SubmittedData s.ClickedLink += s.SubmittedData
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
@ -426,6 +432,7 @@ func PostCampaign(c *Campaign, uid int64) error {
FirstName: t.FirstName, FirstName: t.FirstName,
LastName: t.LastName, LastName: t.LastName,
SendDate: c.LaunchDate, SendDate: c.LaunchDate,
Reported: false,
} }
if c.Status == CAMPAIGN_IN_PROGRESS { if c.Status == CAMPAIGN_IN_PROGRESS {
r.Status = STATUS_SENDING r.Status = STATUS_SENDING

View File

@ -32,6 +32,7 @@ const (
EVENT_OPENED string = "Email Opened" EVENT_OPENED string = "Email Opened"
EVENT_CLICKED string = "Clicked Link" EVENT_CLICKED string = "Clicked Link"
EVENT_DATA_SUBMIT string = "Submitted Data" EVENT_DATA_SUBMIT string = "Submitted Data"
EVENT_REPORTED string = "Email Reported"
EVENT_PROXY_REQUEST string = "Proxied request" EVENT_PROXY_REQUEST string = "Proxied request"
STATUS_SUCCESS string = "Success" STATUS_SUCCESS string = "Success"
STATUS_QUEUED string = "Queued" STATUS_QUEUED string = "Queued"

View File

@ -38,6 +38,7 @@ type Result struct {
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
SendDate time.Time `json:"send_date"` SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"`
} }
// UpdateStatus updates the status of the result in the database // UpdateStatus updates the status of the result in the database
@ -45,6 +46,11 @@ func (r *Result) UpdateStatus(s string) error {
return db.Table("results").Where("id=?", r.Id).Update("status", s).Error return db.Table("results").Where("id=?", r.Id).Update("status", s).Error
} }
// UpdateReported updates when a user reports a campaign
func (r *Result) UpdateReported(s bool) error {
return db.Table("results").Where("id=?", r.Id).Update("reported", s).Error
}
// UpdateGeo updates the latitude and longitude of the result in // UpdateGeo updates the latitude and longitude of the result in
// the database given an IP address // the database given an IP address
func (r *Result) UpdateGeo(addr string) error { func (r *Result) UpdateGeo(addr string) error {

File diff suppressed because one or more lines are too long

7
static/css/main.css vendored
View File

@ -655,6 +655,11 @@ table.dataTable {
color: #f39c12; color: #f39c12;
} }
.color-reported{
font-weight: bold;
color:#45d6ef;
}
.color-success { .color-success {
color: #f05b4f; color: #f05b4f;
} }
@ -666,4 +671,4 @@ table.dataTable {
#resultsMapContainer { #resultsMapContainer {
display: none; display: none;
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ var statuses = {
"Email Opened": { "Email Opened": {
color: "#f9bf3b", color: "#f9bf3b",
label: "label-warning", label: "label-warning",
icon: "fa-envelope", icon: "fa-envelope-open",
point: "ct-point-opened" point: "ct-point-opened"
}, },
"Clicked Link": { "Clicked Link": {
@ -42,6 +42,13 @@ var statuses = {
icon: "fa-exclamation", icon: "fa-exclamation",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
//not a status, but is used for the campaign timeline and user timeline
"Email Reported": {
color: "#45d6ef",
label: "label-info",
icon: "fa-bullhorn",
point: "ct-point-reported"
},
"Error": { "Error": {
color: "#6c7a89", color: "#6c7a89",
label: "label-default", label: "label-default",
@ -95,6 +102,7 @@ var statusMapping = {
"Email Opened": "opened", "Email Opened": "opened",
"Clicked Link": "clicked", "Clicked Link": "clicked",
"Submitted Data": "submitted_data", "Submitted Data": "submitted_data",
"Email Reported": "reported",
} }
// This is an underwhelming attempt at an enum // This is an underwhelming attempt at an enum
@ -282,7 +290,8 @@ function renderTimeline(data) {
"email": data[4], "email": data[4],
"position": data[5], "position": data[5],
"status": data[6], "status": data[6],
"send_date": data[7] "send_date": data[7],
"reported": data[8]
} }
results = '<div class="timeline col-sm-12 well well-lg">' + results = '<div class="timeline col-sm-12 well well-lg">' +
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) + '<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
@ -571,6 +580,9 @@ function poll() {
}); });
$.each(campaign.results, function (i, result) { $.each(campaign.results, function (i, result) {
email_series_data[result.status]++; email_series_data[result.status]++;
if (result.reported) {
email_series_data['Email Reported']++
}
// Backfill status values // Backfill status values
var step = progressListing.indexOf(result.status) var step = progressListing.indexOf(result.status)
for (var i = 0; i < step; i++) { for (var i = 0; i < step; i++) {
@ -595,6 +607,7 @@ function poll() {
data: email_data data: email_data
}) })
}) })
/* Update the datatable */ /* Update the datatable */
resultsTable = $("#resultsTable").DataTable() resultsTable = $("#resultsTable").DataTable()
resultsTable.rows().every(function (i, tableLoop, rowLoop) { resultsTable.rows().every(function (i, tableLoop, rowLoop) {
@ -603,12 +616,13 @@ function poll() {
var rid = rowData[0] var rid = rowData[0]
$.each(campaign.results, function (j, result) { $.each(campaign.results, function (j, result) {
if (result.id == rid) { if (result.id == rid) {
rowData[7] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
rowData[7] = result.reported
rowData[6] = result.status rowData[6] = result.status
resultsTable.row(i).data(rowData) resultsTable.row(i).data(rowData)
if (row.child.isShown()) { if (row.child.isShown()) {
$(row.node()).find("i").removeClass("fa-caret-right") $(row.node()).find("#caret").removeClass("fa-caret-right")
$(row.node()).find("i").addClass("fa-caret-down") $(row.node()).find("#caret").addClass("fa-caret-down")
row.child(renderTimeline(row.data())) row.child(renderTimeline(row.data()))
} }
return false return false
@ -669,13 +683,24 @@ function load() {
"targets": [1] "targets": [1]
}, { }, {
"visible": false, "visible": false,
"targets": [0, 7] "targets": [0, 8]
}, },
{ {
"render": function (data, type, row) { "render": function (data, type, row) {
return createStatusLabel(data, row[7]) return createStatusLabel(data, row[8])
}, },
"targets": [6] "targets": [6]
},
{
className: "text-center",
"render": function (reported, type, row) {
if (reported) {
return "<i class='fa fa-check-circle text-center text-success'></i>"
} else {
return "<i class='fa fa-times-circle text-center text-danger'></i>"
}
},
"targets": [7]
} }
] ]
}); });
@ -688,15 +713,19 @@ function load() {
$.each(campaign.results, function (i, result) { $.each(campaign.results, function (i, result) {
resultsTable.row.add([ resultsTable.row.add([
result.id, result.id,
"<i class=\"fa fa-caret-right\"></i>", "<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
escapeHtml(result.first_name) || "", escapeHtml(result.first_name) || "",
escapeHtml(result.last_name) || "", escapeHtml(result.last_name) || "",
escapeHtml(result.email) || "", escapeHtml(result.email) || "",
escapeHtml(result.position) || "", escapeHtml(result.position) || "",
result.status, result.status,
result.reported,
moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
]) ])
email_series_data[result.status]++; email_series_data[result.status]++;
if (result.reported) {
email_series_data['Email Reported']++
}
// Backfill status values // Backfill status values
var step = progressListing.indexOf(result.status) var step = progressListing.indexOf(result.status)
for (var i = 0; i < step; i++) { for (var i = 0; i < step; i++) {
@ -761,6 +790,7 @@ function load() {
colors: [statuses[status].color, '#dddddd'] colors: [statuses[status].color, '#dddddd']
}) })
}) })
if (use_map) { if (use_map) {
$("#resultsMapContainer").show() $("#resultsMapContainer").show()
map = new Datamap({ map = new Datamap({

View File

@ -324,7 +324,7 @@ $(document).ready(function () {
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else { } else {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a') launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "Reported : " + campaign.stats.reported
} }
campaignTable.row.add([ campaignTable.row.add([
@ -366,4 +366,4 @@ $(document).ready(function () {
return 0; return 0;
}); });
}) })
}) })

View File

@ -1,5 +1,4 @@
var campaigns = [] var campaigns = []
// statuses is a helper map to point result statuses to ui classes // statuses is a helper map to point result statuses to ui classes
var statuses = { var statuses = {
"Email Sent": { "Email Sent": {
@ -29,6 +28,12 @@ var statuses = {
icon: "fa-envelope", icon: "fa-envelope",
point: "ct-point-opened" point: "ct-point-opened"
}, },
"Email Reported": {
color: "#45d6ef",
label: "label-warning",
icon: "fa-bullhorne",
point: "ct-point-reported"
},
"Clicked Link": { "Clicked Link": {
color: "#F39C12", color: "#F39C12",
label: "label-clicked", label: "label-clicked",
@ -80,6 +85,7 @@ var statuses = {
var statsMapping = { var statsMapping = {
"sent": "Email Sent", "sent": "Email Sent",
"opened": "Email Opened", "opened": "Email Opened",
"email_reported": "Email Reported",
"clicked": "Clicked Link", "clicked": "Clicked Link",
"submitted_data": "Submitted Data", "submitted_data": "Submitted Data",
} }
@ -107,16 +113,18 @@ function renderPieChart(chartopts) {
left = chart.plotLeft + pie.center[0], left = chart.plotLeft + pie.center[0],
top = chart.plotTop + pie.center[1]; top = chart.plotTop + pie.center[1];
this.innerText = rend.text(chartopts['data'][0].count, left, top). this.innerText = rend.text(chartopts['data'][0].count, left, top).
attr({ attr({
'text-anchor': 'middle', 'text-anchor': 'middle',
'font-size': '24px', 'font-size': '16px',
'font-weight': 'bold', 'font-weight': 'bold',
'fill': chartopts['colors'][0], 'fill': chartopts['colors'][0],
'font-family': 'Helvetica,Arial,sans-serif' 'font-family': 'Helvetica,Arial,sans-serif'
}).add(); }).add();
}, },
render: function () { render: function () {
this.innerText.attr({ text: chartopts['data'][0].count }) this.innerText.attr({
text: chartopts['data'][0].count
})
} }
} }
}, },
@ -190,6 +198,7 @@ function generateStatsPieCharts(campaigns) {
data: stats_data, data: stats_data,
colors: [statuses[status_label].color, "#dddddd"] colors: [statuses[status_label].color, "#dddddd"]
}) })
stats_data = [] stats_data = []
}); });
} }
@ -289,13 +298,30 @@ $(document).ready(function () {
// Create the overview chart data // Create the overview chart data
campaignTable = $("#campaignTable").DataTable({ campaignTable = $("#campaignTable").DataTable({
columnDefs: [{ columnDefs: [{
orderable: false, orderable: false,
targets: "no-sort" targets: "no-sort"
}, },
{ className: "color-sent", targets: [2] }, {
{ className: "color-opened", targets: [3] }, className: "color-sent",
{ className: "color-clicked", targets: [4] }, targets: [2]
{ className: "color-success", targets: [5] }], },
{
className: "color-opened",
targets: [3]
},
{
className: "color-clicked",
targets: [4]
},
{
className: "color-success",
targets: [5]
},
{
className: "color-reported",
targets: [6]
}
],
order: [ order: [
[1, "desc"] [1, "desc"]
] ]
@ -310,7 +336,7 @@ $(document).ready(function () {
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else { } else {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a') launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "<br><br>" + "Reported : " + campaign.stats.email_reported
} }
// Add it to the table // Add it to the table
campaignTable.row.add([ campaignTable.row.add([
@ -320,6 +346,7 @@ $(document).ready(function () {
campaign.stats.opened, campaign.stats.opened,
campaign.stats.clicked, campaign.stats.clicked,
campaign.stats.submitted_data, campaign.stats.submitted_data,
campaign.stats.email_reported,
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>", "<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\ "<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
<i class='fa fa-bar-chart'></i>\ <i class='fa fa-bar-chart'></i>\
@ -340,4 +367,4 @@ $(document).ready(function () {
.error(function () { .error(function () {
errorFlash("Error fetching campaigns") errorFlash("Error fetching campaigns")
}) })
}) })

View File

@ -3,26 +3,35 @@
<div class="row"> <div class="row">
<div class="col-sm-3 col-md-2 sidebar"> <div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar"> <ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a> <li>
<a href="/">Dashboard</a>
</li> </li>
<li class="active"><a href="/campaigns">Campaigns</a> <li class="active">
<a href="/campaigns">Campaigns</a>
</li> </li>
<li><a href="/users">Users &amp; Groups</a> <li>
<a href="/users">Users &amp; Groups</a>
</li> </li>
<li><a href="/templates">Email Templates</a> <li>
<a href="/templates">Email Templates</a>
</li> </li>
<li><a href="/landing_pages">Landing Pages</a> <li>
<a href="/landing_pages">Landing Pages</a>
</li> </li>
<li><a href="/sending_profiles">Sending Profiles</a> <li>
<a href="/sending_profiles">Sending Profiles</a>
</li> </li>
<li><a href="/settings">Settings</a> <li>
<a href="/settings">Settings</a>
</li> </li>
<li> <li>
<hr> <hr>
</li> </li>
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a> <li>
<a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
</li> </li>
<li><a href="/api/">API Documentation</a> <li>
<a href="/api/">API Documentation</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -47,8 +56,12 @@
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="exportButton"> <ul class="dropdown-menu" aria-labelledby="exportButton">
<li><a href="#" onclick="exportAsCSV('results')">Results</a></li> <li>
<li><a href="#" onclick="exportAsCSV('events')">Raw Events</a></li> <a href="#" onclick="exportAsCSV('results')">Results</a>
</li>
<li>
<a href="#" onclick="exportAsCSV('events')">Raw Events</a>
</li>
</ul> </ul>
</div> </div>
<button id="complete_button" type="button" class="btn btn-blue" data-toggle="tooltip" onclick="completeCampaign()"> <button id="complete_button" type="button" class="btn btn-blue" data-toggle="tooltip" onclick="completeCampaign()">
@ -73,10 +86,13 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div style="height:200px;" class="col-lg-1 col-md-1"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div id="sent_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div id="opened_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div id="clicked_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="reported_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
</div> </div>
<div class="row" id="resultsMapContainer"> <div class="row" id="resultsMapContainer">
<div class="col-md-6"> <div class="col-md-6">
@ -99,6 +115,7 @@
<th>Email</th> <th>Email</th>
<th>Position</th> <th>Position</th>
<th>Status</th> <th>Status</th>
<th class="text-center">Reported</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -43,10 +43,13 @@
<div id="overview_chart" style="height:200px;" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"></div> <div id="overview_chart" style="height:200px;" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"></div>
</div> </div>
<div class="row"> <div class="row">
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div style="height:200px;" class="col-lg-1 col-md-1"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div id="sent_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div id="opened_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div> <div id="clicked_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="email_reported_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
</div> </div>
<div class="row"> <div class="row">
<h2>Recent Campaigns</h2> <h2>Recent Campaigns</h2>
@ -65,6 +68,7 @@
<th class="col-md-1 col-sm-1"><i class="fa fa-envelope-open-o"></i></th> <th class="col-md-1 col-sm-1"><i class="fa fa-envelope-open-o"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-mouse-pointer"></i></th> <th class="col-md-1 col-sm-1"><i class="fa fa-mouse-pointer"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-exclamation-circle"></i></th> <th class="col-md-1 col-sm-1"><i class="fa fa-exclamation-circle"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-bullhorn"></i></th>
<th class="col-md-1 col-sm-1">Status</th> <th class="col-md-1 col-sm-1">Status</th>
<th class="col-md-2 col-sm-2 no-sort"></i></th> <th class="col-md-2 col-sm-2 no-sort"></i></th>
</tr> </tr>