mirror of
https://github.com/axllent/mailpit.git
synced 2025-01-22 03:39:59 +02:00
Merge branch 'feature/search-pagination' into develop
This commit is contained in:
commit
b8de57da27
@ -62,10 +62,11 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
res.Start = start
|
res.Start = start
|
||||||
res.Messages = messages
|
res.Messages = messages
|
||||||
res.Count = len(messages)
|
res.Count = len(messages) // legacy - now undocumented in API specs
|
||||||
res.Total = stats.Total
|
res.Total = stats.Total
|
||||||
res.Unread = stats.Unread
|
res.Unread = stats.Unread
|
||||||
res.Tags = stats.Tags
|
res.Tags = stats.Tags
|
||||||
|
res.MessagesCount = stats.Total
|
||||||
|
|
||||||
bytes, _ := json.Marshal(res)
|
bytes, _ := json.Marshal(res)
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
@ -109,7 +110,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
start, limit := getStartLimit(r)
|
start, limit := getStartLimit(r)
|
||||||
|
|
||||||
messages, err := storage.Search(search, start, limit)
|
messages, results, err := storage.Search(search, start, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, err.Error())
|
httpError(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -121,8 +122,9 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
res.Start = start
|
res.Start = start
|
||||||
res.Messages = messages
|
res.Messages = messages
|
||||||
res.Count = len(messages)
|
res.Count = results // legacy - now undocumented in API specs
|
||||||
res.Total = stats.Total
|
res.Total = stats.Total
|
||||||
|
res.MessagesCount = results
|
||||||
res.Unread = stats.Unread
|
res.Unread = stats.Unread
|
||||||
res.Tags = stats.Tags
|
res.Tags = stats.Tags
|
||||||
|
|
||||||
|
@ -12,9 +12,17 @@ type MessagesSummary struct {
|
|||||||
// Total number of unread messages in mailbox
|
// Total number of unread messages in mailbox
|
||||||
Unread int `json:"unread"`
|
Unread int `json:"unread"`
|
||||||
|
|
||||||
// Number of results returned
|
// Legacy - now undocumented in API specs but left for backwards compatibility.
|
||||||
|
// Removed from API documentation 2023-07-12
|
||||||
|
// swagger:ignore
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
|
|
||||||
|
// Total number of messages matching current query
|
||||||
|
MessagesCount int `json:"messages_count"`
|
||||||
|
|
||||||
|
// // Number of results returned on current page
|
||||||
|
// Count int `json:"count"`
|
||||||
|
|
||||||
// Pagination offset
|
// Pagination offset
|
||||||
Start int `json:"start"`
|
Start int `json:"start"`
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ func WebUIConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
// # Get web UI configuration
|
// # Get web UI configuration
|
||||||
//
|
//
|
||||||
// Returns configuration settings for the web UI.
|
// Returns configuration settings for the web UI.
|
||||||
|
// Intended for web UI only!
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
|
@ -195,7 +195,7 @@ func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, count, m.Count, "wrong search results count")
|
assertEqual(t, count, m.MessagesCount, "wrong search results count")
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertEmailData(t *testing.T) {
|
func insertEmailData(t *testing.T) {
|
||||||
|
@ -26,6 +26,7 @@ export default {
|
|||||||
limit: 50,
|
limit: 50,
|
||||||
total: 0,
|
total: 0,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
|
messagesCount: 0,
|
||||||
start: 0,
|
start: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
tags: [],
|
tags: [],
|
||||||
@ -75,7 +76,7 @@ export default {
|
|||||||
return this.start > 0
|
return this.start > 0
|
||||||
},
|
},
|
||||||
canNext: function () {
|
canNext: function () {
|
||||||
return this.total > (this.start + this.count)
|
return this.messagesCount > (this.start + this.count)
|
||||||
},
|
},
|
||||||
unreadInSearch: function () {
|
unreadInSearch: function () {
|
||||||
if (!this.searching) {
|
if (!this.searching) {
|
||||||
@ -146,11 +147,12 @@ export default {
|
|||||||
let uri = 'api/v1/messages'
|
let uri = 'api/v1/messages'
|
||||||
if (self.search) {
|
if (self.search) {
|
||||||
self.searching = true
|
self.searching = true
|
||||||
self.items = []
|
|
||||||
uri = 'api/v1/search'
|
uri = 'api/v1/search'
|
||||||
self.start = 0 // search is displayed on one page
|
|
||||||
params['query'] = self.search
|
params['query'] = self.search
|
||||||
params['limit'] = 200
|
params['limit'] = self.limit
|
||||||
|
if (self.start > 0) {
|
||||||
|
params['start'] = self.start
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.searching = false
|
self.searching = false
|
||||||
params['limit'] = self.limit
|
params['limit'] = self.limit
|
||||||
@ -162,7 +164,8 @@ export default {
|
|||||||
self.get(uri, params, function (response) {
|
self.get(uri, params, function (response) {
|
||||||
self.total = response.data.total
|
self.total = response.data.total
|
||||||
self.unread = response.data.unread
|
self.unread = response.data.unread
|
||||||
self.count = response.data.count
|
self.count = response.data.messages.length
|
||||||
|
self.messagesCount = response.data.messages_count
|
||||||
self.start = response.data.start
|
self.start = response.data.start
|
||||||
self.items = response.data.messages
|
self.items = response.data.messages
|
||||||
self.tags = response.data.tags
|
self.tags = response.data.tags
|
||||||
@ -194,6 +197,7 @@ export default {
|
|||||||
|
|
||||||
doSearch: function (e) {
|
doSearch: function (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
this.start = 0
|
||||||
this.loadMessages()
|
this.loadMessages()
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -204,13 +208,14 @@ export default {
|
|||||||
}
|
}
|
||||||
this.search = 'tag:' + tag
|
this.search = 'tag:' + tag
|
||||||
window.location.hash = ""
|
window.location.hash = ""
|
||||||
|
this.start = 0
|
||||||
this.loadMessages()
|
this.loadMessages()
|
||||||
},
|
},
|
||||||
|
|
||||||
resetSearch: function (e) {
|
resetSearch: function (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.search = ''
|
this.search = ''
|
||||||
this.scrollInPlace = true
|
this.start = 0
|
||||||
this.loadMessages()
|
this.loadMessages()
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -459,9 +464,9 @@ export default {
|
|||||||
|
|
||||||
// websocket connect
|
// websocket connect
|
||||||
connect: function () {
|
connect: function () {
|
||||||
let wsproto = location.protocol == 'https:' ? 'wss' : 'ws'
|
let proto = location.protocol == 'https:' ? 'wss' : 'ws'
|
||||||
let ws = new WebSocket(
|
let ws = new WebSocket(
|
||||||
wsproto + "://" + document.location.host + document.location.pathname + "api/events"
|
proto + "://" + document.location.host + document.location.pathname + "api/events"
|
||||||
)
|
)
|
||||||
let self = this
|
let self = this
|
||||||
ws.onmessage = function (e) {
|
ws.onmessage = function (e) {
|
||||||
@ -809,7 +814,7 @@ export default {
|
|||||||
<i class="bi bi-check2-square"></i>
|
<i class="bi bi-check2-square"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<select v-model="limit" v-on:change="loadMessages" v-if="!searching"
|
<select v-model="limit" v-on:change="loadMessages"
|
||||||
class="form-select form-select-sm d-none d-md-inline w-auto me-2">
|
class="form-select form-select-sm d-none d-md-inline w-auto me-2">
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
@ -817,23 +822,18 @@ export default {
|
|||||||
<option value="200">200</option>
|
<option value="200">200</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<span v-if="searching">
|
<small>
|
||||||
<b>{{ formatNumber(items.length) }} result<template v-if="items.length != 1">s</template></b>
|
{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small>
|
||||||
</span>
|
{{ formatNumber(messagesCount) }}
|
||||||
<span v-else>
|
</small>
|
||||||
<small>
|
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev"
|
||||||
{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small>
|
:title="'View previous ' + limit + ' messages'">
|
||||||
{{ formatNumber(total) }}
|
<i class="bi bi-caret-left-fill"></i>
|
||||||
</small>
|
</button>
|
||||||
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev" v-if="!searching"
|
<button class="btn btn-outline-light" :disabled="!canNext" v-on:click="viewNext"
|
||||||
:title="'View previous ' + limit + ' messages'">
|
:title="'View next ' + limit + ' messages'">
|
||||||
<i class="bi bi-caret-left-fill"></i>
|
<i class="bi bi-caret-right-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-light" :disabled="!canNext" v-on:click="viewNext" v-if="!searching"
|
|
||||||
:title="'View next ' + limit + ' messages'">
|
|
||||||
<i class="bi bi-caret-right-fill"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row flex-fill" style="min-height:0">
|
<div class="row flex-fill" style="min-height:0">
|
||||||
|
@ -23,11 +23,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
srcURI: false,
|
srcURI: false,
|
||||||
iframes: [], // for resizing
|
iframes: [], // for resizing
|
||||||
showTags: false, // to force rerendering of component
|
showTags: false, // to force re-rendering of component
|
||||||
|
canSaveTags: false, // prevent auto-saving tags on render
|
||||||
messageTags: [],
|
messageTags: [],
|
||||||
allTags: [],
|
allTags: [],
|
||||||
loadHeaders: false,
|
loadHeaders: false,
|
||||||
showMobileBtns: false,
|
showMobileButtons: false,
|
||||||
scaleHTMLPreview: 'display',
|
scaleHTMLPreview: 'display',
|
||||||
// keys names match bootstrap icon names
|
// keys names match bootstrap icon names
|
||||||
responsiveSizes: {
|
responsiveSizes: {
|
||||||
@ -39,20 +40,25 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
// handle changes to the URL messageID
|
||||||
message: {
|
message: {
|
||||||
handler() {
|
handler() {
|
||||||
let self = this
|
let self = this
|
||||||
self.showTags = false
|
self.showTags = false
|
||||||
|
self.canSaveTags = false
|
||||||
self.messageTags = self.message.Tags
|
self.messageTags = self.message.Tags
|
||||||
self.allTags = self.existingTags
|
self.allTags = self.existingTags
|
||||||
self.loadHeaders = false
|
self.loadHeaders = false
|
||||||
self.scaleHTMLPreview = 'display';// default view
|
self.scaleHTMLPreview = 'display' // default view
|
||||||
// delay to select first tab and add HTML highlighting (prev/next)
|
// delay to select first tab and add HTML highlighting (prev/next)
|
||||||
self.$nextTick(function () {
|
self.$nextTick(function () {
|
||||||
self.renderUI()
|
self.renderUI()
|
||||||
self.showTags = true
|
self.showTags = true
|
||||||
self.$nextTick(function () {
|
self.$nextTick(function () {
|
||||||
Tags.init("select[multiple]")
|
Tags.init("select[multiple]")
|
||||||
|
window.setTimeout(function () {
|
||||||
|
self.canSaveTags = true
|
||||||
|
}, 200)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -60,8 +66,8 @@ export default {
|
|||||||
immediate: true
|
immediate: true
|
||||||
},
|
},
|
||||||
messageTags() {
|
messageTags() {
|
||||||
// save changed to tags
|
// save changes to tags
|
||||||
if (this.showTags) {
|
if (this.canSaveTags) {
|
||||||
this.saveTags()
|
this.saveTags()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -78,6 +84,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
let self = this
|
let self = this
|
||||||
self.showTags = false
|
self.showTags = false
|
||||||
|
self.canSaveTags = false
|
||||||
self.allTags = self.existingTags
|
self.allTags = self.existingTags
|
||||||
window.addEventListener("resize", self.resizeIframes)
|
window.addEventListener("resize", self.resizeIframes)
|
||||||
self.renderUI()
|
self.renderUI()
|
||||||
@ -95,7 +102,12 @@ export default {
|
|||||||
|
|
||||||
self.showTags = true
|
self.showTags = true
|
||||||
self.$nextTick(function () {
|
self.$nextTick(function () {
|
||||||
Tags.init("select[multiple]")
|
self.$nextTick(function () {
|
||||||
|
Tags.init('select[multiple]')
|
||||||
|
window.setTimeout(function () {
|
||||||
|
self.canSaveTags = true
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -104,6 +116,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
isHTMLTabSelected: function () {
|
||||||
|
this.showMobileButtons = this.$refs.navhtml
|
||||||
|
&& this.$refs.navhtml.classList.contains('active')
|
||||||
|
},
|
||||||
renderUI: function () {
|
renderUI: function () {
|
||||||
let self = this
|
let self = this
|
||||||
// click the first non-disabled tab
|
// click the first non-disabled tab
|
||||||
@ -111,6 +127,14 @@ export default {
|
|||||||
document.activeElement.blur() // blur focus
|
document.activeElement.blur() // blur focus
|
||||||
document.getElementById('message-view').scrollTop = 0
|
document.getElementById('message-view').scrollTop = 0
|
||||||
|
|
||||||
|
self.isHTMLTabSelected()
|
||||||
|
|
||||||
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(function (listObj) {
|
||||||
|
listObj.addEventListener('shown.bs.tab', function (event) {
|
||||||
|
self.isHTMLTabSelected()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// delay 0.2s until vue has rendered the iframe content
|
// delay 0.2s until vue has rendered the iframe content
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
let p = document.getElementById('preview-html')
|
let p = document.getElementById('preview-html')
|
||||||
@ -311,29 +335,30 @@ export default {
|
|||||||
<nav>
|
<nav>
|
||||||
<div class="nav nav-tabs my-3" id="nav-tab" role="tablist">
|
<div class="nav nav-tabs my-3" id="nav-tab" role="tablist">
|
||||||
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab" data-bs-target="#nav-html" type="button"
|
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab" data-bs-target="#nav-html" type="button"
|
||||||
role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML"
|
role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML" ref="navhtml"
|
||||||
v-on:click="showMobileBtns = true; resizeIframes()">HTML</button>
|
v-on:click="resizeIframes()">HTML</button>
|
||||||
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source"
|
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source"
|
||||||
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if="message.HTML"
|
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if="message.HTML">
|
||||||
v-on:click=" showMobileBtns = false">
|
|
||||||
HTML <span class="d-sm-none">Src</span><span class="d-none d-sm-inline">Source</span>
|
HTML <span class="d-sm-none">Src</span><span class="d-none d-sm-inline">Source</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab" data-bs-target="#nav-plain-text"
|
<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab" data-bs-target="#nav-plain-text"
|
||||||
type="button" role="tab" aria-controls="nav-plain-text" aria-selected="false"
|
type="button" role="tab" aria-controls="nav-plain-text" aria-selected="false"
|
||||||
:class="message.HTML == '' ? 'show' : ''" v-on:click=" showMobileBtns = false">Text</button>
|
:class="message.HTML == '' ? 'show' : ''">
|
||||||
|
Text
|
||||||
|
</button>
|
||||||
<button class="nav-link" id="nav-headers-tab" data-bs-toggle="tab" data-bs-target="#nav-headers"
|
<button class="nav-link" id="nav-headers-tab" data-bs-toggle="tab" data-bs-target="#nav-headers"
|
||||||
type="button" role="tab" aria-controls="nav-headers" aria-selected="false"
|
type="button" role="tab" aria-controls="nav-headers" aria-selected="false">
|
||||||
v-on:click=" showMobileBtns = false">
|
|
||||||
<span class="d-sm-none">Hdrs</span><span class="d-none d-sm-inline">Headers</span>
|
<span class="d-sm-none">Hdrs</span><span class="d-none d-sm-inline">Headers</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-link" id="nav-raw-tab" data-bs-toggle="tab" data-bs-target="#nav-raw" type="button"
|
<button class="nav-link" id="nav-raw-tab" data-bs-toggle="tab" data-bs-target="#nav-raw" type="button"
|
||||||
role="tab" aria-controls="nav-raw" aria-selected="false"
|
role="tab" aria-controls="nav-raw" aria-selected="false">
|
||||||
v-on:click=" showMobileBtns = false">Raw</button>
|
Raw
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="d-none d-lg-block ms-auto me-2" v-if="showMobileBtns">
|
<div class="d-none d-lg-block ms-auto me-2" v-if="showMobileButtons">
|
||||||
<template v-for=" vals, key in responsiveSizes ">
|
<template v-for="vals, key in responsiveSizes">
|
||||||
<button class="btn" :disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
|
<button class="btn" :disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
|
||||||
v-on:click=" scaleHTMLPreview = key">
|
v-on:click="scaleHTMLPreview = key">
|
||||||
<i class="bi" :class="'bi-' + key"></i>
|
<i class="bi" :class="'bi-' + key"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -489,7 +489,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/webui": {
|
"/api/v1/webui": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns configuration settings for the web UI.",
|
"description": "Returns configuration settings for the web UI.\nIntended for web UI only!",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -789,12 +789,6 @@
|
|||||||
"description": "MessagesSummary is a summary of a list of messages",
|
"description": "MessagesSummary is a summary of a list of messages",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"count": {
|
|
||||||
"description": "Number of results returned",
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "Count"
|
|
||||||
},
|
|
||||||
"messages": {
|
"messages": {
|
||||||
"description": "Messages summary\nin:body",
|
"description": "Messages summary\nin:body",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -803,6 +797,12 @@
|
|||||||
},
|
},
|
||||||
"x-go-name": "Messages"
|
"x-go-name": "Messages"
|
||||||
},
|
},
|
||||||
|
"messages_count": {
|
||||||
|
"description": "Total number of messages matching current query",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "MessagesCount"
|
||||||
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"description": "Pagination offset",
|
"description": "Pagination offset",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -926,10 +926,10 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"BinaryResponse": {
|
"BinaryResponse": {
|
||||||
"description": "Binary data reponse inherits the attachment's content type"
|
"description": "Binary data response inherits the attachment's content type"
|
||||||
},
|
},
|
||||||
"ErrorResponse": {
|
"ErrorResponse": {
|
||||||
"description": "Error reponse"
|
"description": "Error response"
|
||||||
},
|
},
|
||||||
"InfoResponse": {
|
"InfoResponse": {
|
||||||
"description": "Application information",
|
"description": "Application information",
|
||||||
@ -949,7 +949,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"OKResponse": {
|
"OKResponse": {
|
||||||
"description": "Plain text \"ok\" reponse"
|
"description": "Plain text \"ok\" response"
|
||||||
},
|
},
|
||||||
"TextResponse": {
|
"TextResponse": {
|
||||||
"description": "Plain text response"
|
"description": "Plain text response"
|
||||||
|
@ -384,9 +384,14 @@ func List(start, limit int) ([]MessageSummary, error) {
|
|||||||
// The search is broken up by segments (exact phrases can be quoted), and interprets specific terms such as:
|
// The search is broken up by segments (exact phrases can be quoted), and interprets specific terms such as:
|
||||||
// is:read, is:unread, has:attachment, to:<term>, from:<term> & subject:<term>
|
// is:read, is:unread, has:attachment, to:<term>, from:<term> & subject:<term>
|
||||||
// Negative searches also also included by prefixing the search term with a `-` or `!`
|
// Negative searches also also included by prefixing the search term with a `-` or `!`
|
||||||
func Search(search string, start, limit int) ([]MessageSummary, error) {
|
func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||||
results := []MessageSummary{}
|
results := []MessageSummary{}
|
||||||
|
allResults := []MessageSummary{}
|
||||||
tsStart := time.Now()
|
tsStart := time.Now()
|
||||||
|
nrResults := 0
|
||||||
|
if limit < 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
s := strings.ToLower(search)
|
s := strings.ToLower(search)
|
||||||
// add another quote if missing closing quote
|
// add another quote if missing closing quote
|
||||||
@ -398,11 +403,11 @@ func Search(search string, start, limit int) ([]MessageSummary, error) {
|
|||||||
p := shellwords.NewParser()
|
p := shellwords.NewParser()
|
||||||
args, err := p.Parse(s)
|
args, err := p.Parse(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return results, errors.New("Your search contains invalid characters")
|
return results, nrResults, errors.New("Your search contains invalid characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate the SQL based on arguments
|
// generate the SQL based on arguments
|
||||||
q := searchParser(args, start, limit)
|
q := searchParser(args)
|
||||||
|
|
||||||
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||||
var created int64
|
var created int64
|
||||||
@ -440,18 +445,29 @@ func Search(search string, start, limit int) ([]MessageSummary, error) {
|
|||||||
em.Attachments = attachments
|
em.Attachments = attachments
|
||||||
em.Read = read == 1
|
em.Read = read == 1
|
||||||
|
|
||||||
results = append(results, em)
|
allResults = append(allResults, em)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return results, err
|
return results, nrResults, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbLastAction = time.Now()
|
||||||
|
|
||||||
|
nrResults = len(allResults)
|
||||||
|
|
||||||
|
if nrResults > start {
|
||||||
|
end := nrResults
|
||||||
|
if nrResults >= start+limit {
|
||||||
|
end = start + limit
|
||||||
|
}
|
||||||
|
|
||||||
|
results = allResults[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(tsStart)
|
elapsed := time.Since(tsStart)
|
||||||
|
|
||||||
logger.Log().Debugf("[db] search for \"%s\" in %s", search, elapsed)
|
logger.Log().Debugf("[db] search for \"%s\" in %s", search, elapsed)
|
||||||
|
|
||||||
dbLastAction = time.Now()
|
return results, nrResults, err
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMessage returns a Message generated from the mailbox_data collection.
|
// GetMessage returns a Message generated from the mailbox_data collection.
|
||||||
|
@ -168,9 +168,9 @@ func TestSearch(t *testing.T) {
|
|||||||
|
|
||||||
for i := 1; i < 51; i++ {
|
for i := 1; i < 51; i++ {
|
||||||
// search a random something that will return a single result
|
// search a random something that will return a single result
|
||||||
searchIndx := rand.Intn(4) + 1
|
searchIdx := rand.Intn(4) + 1
|
||||||
var search string
|
var search string
|
||||||
switch searchIndx {
|
switch searchIdx {
|
||||||
case 1:
|
case 1:
|
||||||
search = fmt.Sprintf("from-%d@example.com", i)
|
search = fmt.Sprintf("from-%d@example.com", i)
|
||||||
case 2:
|
case 2:
|
||||||
@ -181,7 +181,7 @@ func TestSearch(t *testing.T) {
|
|||||||
search = fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i)
|
search = fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
summaries, err := Search(search, 0, 100)
|
summaries, _, err := Search(search, 0, 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@ -196,8 +196,8 @@ func TestSearch(t *testing.T) {
|
|||||||
assertEqual(t, summaries[0].Subject, fmt.Sprintf("Subject line %d end", i), "\"Subject\" does not match")
|
assertEqual(t, summaries[0].Subject, fmt.Sprintf("Subject line %d end", i), "\"Subject\" does not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
// search something that will return 200 rsults
|
// search something that will return 200 results
|
||||||
summaries, err := Search("This is the email body", 0, testRuns)
|
summaries, _, err := Search("This is the email body", 0, testRuns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -8,25 +8,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// SearchParser returns the SQL syntax for the database search based on the search arguments
|
// SearchParser returns the SQL syntax for the database search based on the search arguments
|
||||||
func searchParser(args []string, start, limit int) *sqlf.Stmt {
|
func searchParser(args []string) *sqlf.Stmt {
|
||||||
if limit == 0 {
|
|
||||||
limit = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
q := sqlf.From("mailbox").
|
q := sqlf.From("mailbox").
|
||||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags,
|
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags,
|
||||||
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
|
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
|
||||||
IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON,
|
IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON,
|
||||||
IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON,
|
IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON,
|
||||||
IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON
|
IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON
|
||||||
`).
|
`).OrderBy("Created DESC")
|
||||||
OrderBy("Created DESC").
|
|
||||||
Limit(limit).
|
|
||||||
Offset(start)
|
|
||||||
|
|
||||||
if limit > 0 {
|
|
||||||
q = q.Limit(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, w := range args {
|
for _, w := range args {
|
||||||
if cleanString(w) == "" {
|
if cleanString(w) == "" {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user